From affcdba145c4b59a85195e9cf0a4f31f9cf5c5d7 Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 3 Sep 2024 11:58:19 -0700 Subject: [PATCH] sync strategy for landline logs --- apps/daimo-mobile/src/sync/sync.ts | 19 +- apps/daimo-mobile/src/sync/syncLandline.ts | 86 ++++++++ apps/daimo-mobile/test/sync.test.ts | 219 +++++++++++++++++++++ 3 files changed, 316 insertions(+), 8 deletions(-) create mode 100644 apps/daimo-mobile/src/sync/syncLandline.ts create mode 100644 apps/daimo-mobile/test/sync.test.ts diff --git a/apps/daimo-mobile/src/sync/sync.ts b/apps/daimo-mobile/src/sync/sync.ts index 09ee21fb9..5497fff1c 100644 --- a/apps/daimo-mobile/src/sync/sync.ts +++ b/apps/daimo-mobile/src/sync/sync.ts @@ -13,6 +13,7 @@ import { daimoChainFromId } from "@daimo/contract"; import * as SplashScreen from "expo-splash-screen"; import { getNetworkState, updateNetworkState } from "./networkState"; +import { addLandlineTransfers } from "./syncLandline"; import { i18NLocale } from "../i18n"; import { getAccountManager } from "../logic/accountManager"; import { SEND_DEADLINE_SECS } from "../logic/opSender"; @@ -170,7 +171,7 @@ async function fetchSync( landlineSessionURL: result.landlineSessionURL, numLandlineAccounts: (result.landlineAccounts || []).length, }; - // console.log(`[SYNC] got history ${JSON.stringify(syncSummary)}`); + console.log(`[SYNC] got history ${JSON.stringify(syncSummary)}`); // Validation assert(result.address === account.address, "wrong address"); @@ -335,15 +336,17 @@ function addNamedAccounts(old: EAccount[], found: EAccount[]): EAccount[] { /** Add transfers based on new Transfer event logs */ function addTransfers( - old: TransferClog[], - logs: TransferClog[] + oldLogs: TransferClog[], + newLogs: TransferClog[] ): TransferClog[] { - // Sort new logs + const { logs, remaining } = addLandlineTransfers(oldLogs, newLogs); + + logs.push(...remaining); + + // Sort logs logs.sort((a, b) => { - if (a.blockNumber !== b.blockNumber) return a.blockNumber! - b.blockNumber!; - return a.logIndex! - b.logIndex!; + return a.timestamp - b.timestamp; }); - // old finalized logs + new logs - return [...old, ...logs]; + return logs; } diff --git a/apps/daimo-mobile/src/sync/syncLandline.ts b/apps/daimo-mobile/src/sync/syncLandline.ts new file mode 100644 index 000000000..c5b3c7b3a --- /dev/null +++ b/apps/daimo-mobile/src/sync/syncLandline.ts @@ -0,0 +1,86 @@ +import { + TransferClog, + TransferSwapClog, + getTransferClogType, +} from "@daimo/common"; + +/** + * All old landline clogs should be bundled into a single clog with the most + * up-to-date offchainTransfer. + * + * Landline TransferClog sync strategy: + * 1. If an old log matches by tx hash, then it is the on-chain counterpart + * of the incoming landline clog. Keep the on-chain part of the clog and + * update the offchainTransfer to the incoming landline clog's. + * 2. If an old log matches by just the transferID, then it is a potentially + * outdated landline clog. Replace the old clog with the incoming landline clog. + */ +export function addLandlineTransfers( + oldLogs: TransferClog[], + newLogs: TransferClog[] +): { + logs: TransferClog[]; + remaining: TransferClog[]; +} { + // Separate new landline clogs from other clogs + const landlineLogs: TransferSwapClog[] = []; + const remainingLogs: TransferClog[] = []; + for (const log of newLogs) { + if (getTransferClogType(log) === "landline") { + landlineLogs.push(log as TransferSwapClog); + } else { + remainingLogs.push(log); + } + } + + // Flag to mark which old logs have been replaced by a landline log + const replacedOldLog: boolean[] = Array(oldLogs.length).fill(false); + + const updatedLandlineLogs: TransferSwapClog[] = []; + for (const landlineLog of landlineLogs) { + const matchingTransfers: TransferSwapClog[] = []; + for (let i = 0; i < oldLogs.length; i++) { + const oldLog = oldLogs[i]; + if ( + getTransferClogType(oldLog) !== "landline" && + getTransferClogType(oldLog) !== "transfer" + ) { + continue; + } + + const oldSwapLog = oldLog as TransferSwapClog; + + // All old logs which represent the same landline transfer should be replaced + if (landlineLog.txHash && oldSwapLog.txHash === landlineLog.txHash) { + matchingTransfers.push(oldSwapLog); + replacedOldLog[i] = true; + } else if ( + oldSwapLog.offchainTransfer?.transferID === + landlineLog.offchainTransfer!.transferID + ) { + replacedOldLog[i] = true; + } + } + + // Replace all old logs with a single updated landline log + if (matchingTransfers.length > 0) { + const updatedLog: TransferSwapClog = { + ...matchingTransfers[matchingTransfers.length - 1], + offchainTransfer: landlineLog.offchainTransfer, + }; + updatedLandlineLogs.push(updatedLog); + } else { + updatedLandlineLogs.push(landlineLog); + } + } + + const allLogs: TransferClog[] = []; + for (let i = 0; i < oldLogs.length; i++) { + if (!replacedOldLog[i]) { + allLogs.push(oldLogs[i]); + } + } + allLogs.push(...updatedLandlineLogs); + + return { logs: allLogs, remaining: remainingLogs }; +} diff --git a/apps/daimo-mobile/test/sync.test.ts b/apps/daimo-mobile/test/sync.test.ts new file mode 100644 index 000000000..cd6354822 --- /dev/null +++ b/apps/daimo-mobile/test/sync.test.ts @@ -0,0 +1,219 @@ +import { OpStatus, TransferClog, TransferSwapClog } from "@daimo/common"; + +import { addLandlineTransfers } from "../src/sync/syncLandline"; + +describe("addLandlineTransfers", () => { + it("adds a new landline deposit clog when there are no existing clogs", () => { + const oldLogs: TransferClog[] = []; + const newLandlineClog: TransferSwapClog = { + type: "transfer", + from: "0x1985EA6E9c68E1C272d8209f3B478AC2Fdb25c87", + to: "0x6af35dF65594398726140cf1bf0339e94c7A817F", + amount: 1000000, + timestamp: 1234567890, + status: OpStatus.confirmed, + offchainTransfer: { + type: "landline", + transferType: "deposit", + status: "processing", + accountID: "asdf-asdf-asdf-asdf-asdf", + timeStart: 1234567890, + }, + }; + + const result = addLandlineTransfers(oldLogs, [newLandlineClog]); + + expect(result.logs).toEqual([newLandlineClog]); + expect(result.remaining).toEqual([]); + }); + + it("replaces an existing landline deposit clog with a new one", () => { + const oldLandlineClog: TransferSwapClog = { + type: "transfer", + from: "0x1985EA6E9c68E1C272d8209f3B478AC2Fdb25c87", + to: "0x6af35dF65594398726140cf1bf0339e94c7A817F", + amount: 1000000, + timestamp: 1234567890, + status: OpStatus.confirmed, + offchainTransfer: { + type: "landline", + transferType: "deposit", + status: "processing", + accountID: "asdf-asdf-asdf-asdf-asdf", + timeStart: 1234567890, + }, + }; + + const newLandlineClog: TransferSwapClog = { + ...oldLandlineClog, + status: OpStatus.failed, + offchainTransfer: { + type: "landline", + transferType: "deposit", + status: "failed", + statusMessage: "Failed to deposit", + accountID: "asdf-asdf-asdf-asdf-asdf", + timeStart: 1234567890, + timeExpected: 1234569999, + }, + }; + + const result = addLandlineTransfers([oldLandlineClog], [newLandlineClog]); + + expect(result.logs).toEqual([newLandlineClog]); + expect(result.remaining).toEqual([]); + }); + + it("merges a transfer clog and an old landline clog with a new landline clog", () => { + const oldTransferClog: TransferSwapClog = { + type: "transfer", + from: "0x1985EA6E9c68E1C272d8209f3B478AC2Fdb25c87", + to: "0x6af35dF65594398726140cf1bf0339e94c7A817F", + amount: 1000000, + timestamp: 1234570000, + status: OpStatus.confirmed, + txHash: + "0x1d6e083a6009de3dc3672f2dd799e52604d819c5b98e3beb77c50ec259630060", + }; + + const oldLandlineClog: TransferSwapClog = { + type: "transfer", + from: "0x1985EA6E9c68E1C272d8209f3B478AC2Fdb25c87", + to: "0x6af35dF65594398726140cf1bf0339e94c7A817F", + amount: 1000000, + timestamp: 1234567890, + status: OpStatus.confirmed, + offchainTransfer: { + type: "landline", + transferType: "deposit", + status: "processing", + accountID: "asdf-asdf-asdf-asdf-asdf", + timeStart: 1234567890, + }, + }; + + const newLandlineClog: TransferSwapClog = { + ...oldLandlineClog, + status: OpStatus.confirmed, + txHash: + "0x1d6e083a6009de3dc3672f2dd799e52604d819c5b98e3beb77c50ec259630060", + offchainTransfer: { + type: "landline", + transferType: "deposit", + status: "completed", + accountID: "asdf-asdf-asdf-asdf-asdf", + timeStart: 1234567890, + timeExpected: 1234569999, + timeFinish: 1234570000, + }, + }; + + const result = addLandlineTransfers( + [oldTransferClog, oldLandlineClog], + [newLandlineClog] + ); + + // The on-chain part of the old transfer should be combined with the + // updated offchain part of the new landline clog + const expectedLog: TransferSwapClog = { + ...oldTransferClog, + offchainTransfer: newLandlineClog.offchainTransfer, + }; + expect(result.logs).toEqual([expectedLog]); + expect(result.remaining).toEqual([]); + }); + + it("merges a new landline withdrawal clog with an old transfer clog", () => { + const oldTransferClog: TransferSwapClog = { + type: "transfer", + from: "0x4D350d99364634e07B01a9986662787DD3755F0A", + to: "0xf8736A44a2420d856d28B9B2f8374973755CcdB5", + amount: 1230000, + timestamp: 1234567890, + status: OpStatus.confirmed, + txHash: + "0x95c66cac0607b2c076f85b935d8b4df801ecf15bcb6d9f58afc324d132e6b8b0", + }; + + const newLandlineClog: TransferSwapClog = { + type: "transfer", + from: "0x4D350d99364634e07B01a9986662787DD3755F0A", + to: "0xf8736A44a2420d856d28B9B2f8374973755CcdB5", + amount: 1230000, + timestamp: 1234567999, + status: OpStatus.confirmed, + txHash: + "0x95c66cac0607b2c076f85b935d8b4df801ecf15bcb6d9f58afc324d132e6b8b0", + offchainTransfer: { + type: "landline", + transferType: "withdrawal", + status: "processing", + accountID: "asdf-asdf-asdf-asdf-asdf", + timeStart: 1234567999, + }, + }; + + const result = addLandlineTransfers([oldTransferClog], [newLandlineClog]); + console.log(result.logs); + + // The on-chain part of the old transfer should be combined with the + // updated offchain part of the new landline clog + const expectedLog: TransferSwapClog = { + ...oldTransferClog, + offchainTransfer: newLandlineClog.offchainTransfer, + }; + expect(result.logs).toEqual([expectedLog]); + expect(result.remaining).toEqual([]); + }); + + it("merges a new landline withdrawal clog with an old landline clog", () => { + const oldLandlineClog: TransferSwapClog = { + type: "transfer", + from: "0x4D350d99364634e07B01a9986662787DD3755F0A", + to: "0xf8736A44a2420d856d28B9B2f8374973755CcdB5", + amount: 1230000, + timestamp: 1234567890, + status: OpStatus.confirmed, + txHash: + "0x95c66cac0607b2c076f85b935d8b4df801ecf15bcb6d9f58afc324d132e6b8b0", + offchainTransfer: { + type: "landline", + transferType: "withdrawal", + status: "processing", + accountID: "asdf-asdf-asdf-asdf-asdf", + timeStart: 1234567999, + }, + }; + + const newLandlineClog: TransferSwapClog = { + type: "transfer", + from: "0x4D350d99364634e07B01a9986662787DD3755F0A", + to: "0xf8736A44a2420d856d28B9B2f8374973755CcdB5", + amount: 1230000, + timestamp: 1234570000, + status: OpStatus.confirmed, + txHash: + "0x95c66cac0607b2c076f85b935d8b4df801ecf15bcb6d9f58afc324d132e6b8b0", + offchainTransfer: { + type: "landline", + transferType: "withdrawal", + status: "completed", + accountID: "asdf-asdf-asdf-asdf-asdf", + timeStart: 1234567999, + timeExpected: 1234569999, + timeFinish: 1234570000, + }, + }; + + const result = addLandlineTransfers([oldLandlineClog], [newLandlineClog]); + + // The on-chain part of the old transfer should be combined with the + // updated offchain part of the new landline clog + const expectedLog: TransferSwapClog = { + ...oldLandlineClog, + offchainTransfer: newLandlineClog.offchainTransfer, + }; + expect(result.logs).toEqual([expectedLog]); + expect(result.remaining).toEqual([]); + }); +});