|
| 1 | +import type { Account, SignedOperation } from "@ledgerhq/types-live"; |
| 2 | +import { setEnv } from "@shared/env"; |
| 3 | +import { hasSignContext, recallSignContext, rememberSignContext } from "./signContext"; |
| 4 | +import { buildBroadcastCommonEvent } from "./eventBuilders"; |
| 5 | +import { TransactionDataSource, TransactionFlow } from "./logEvent"; |
| 6 | + |
| 7 | +const account = (id: string, family: string, ticker: string) => |
| 8 | + ({ id: "acc", type: "Account", currency: { id, family, ticker } }) as unknown as Account; |
| 9 | + |
| 10 | +const signed = (op: Record<string, unknown>) => |
| 11 | + ({ signature: "0xsig", operation: op }) as unknown as SignedOperation; |
| 12 | + |
| 13 | +const attribution = (mainAccount: Account) => ({ |
| 14 | + account: mainAccount, |
| 15 | + mainAccount, |
| 16 | + flow: TransactionFlow.Send, |
| 17 | +}); |
| 18 | + |
| 19 | +beforeEach(() => setEnv("LEDGER_CLIENT_VERSION", "llc/test")); |
| 20 | + |
| 21 | +describe("rememberSignContext / recallSignContext", () => { |
| 22 | + it("carries the exact action and the delegation target across the stages", () => { |
| 23 | + const signedOperation = signed({ type: "DELEGATE", extra: {} }); |
| 24 | + rememberSignContext(signedOperation, "solana", { |
| 25 | + family: "solana", |
| 26 | + model: { kind: "stake.createAccount", uiState: { voteAccAddr: "voteAcc" } }, |
| 27 | + }); |
| 28 | + |
| 29 | + expect(recallSignContext(signedOperation)).toEqual({ |
| 30 | + earnTransactionType: "delegate", |
| 31 | + rawTransactionType: "stake.createAccount", |
| 32 | + validators: ["voteAcc"], |
| 33 | + isSendMax: false, |
| 34 | + }); |
| 35 | + }); |
| 36 | + |
| 37 | + it("does not correlate a different signed operation", () => { |
| 38 | + rememberSignContext(signed({ type: "DELEGATE" }), "cosmos", { |
| 39 | + family: "cosmos", |
| 40 | + mode: "delegate", |
| 41 | + }); |
| 42 | + |
| 43 | + expect(recallSignContext(signed({ type: "DELEGATE" }))).toBeUndefined(); |
| 44 | + expect(recallSignContext(undefined)).toBeUndefined(); |
| 45 | + }); |
| 46 | + |
| 47 | + it("stores nothing for a non-staking transaction", () => { |
| 48 | + const signedOperation = signed({ type: "OUT" }); |
| 49 | + rememberSignContext(signedOperation, "cosmos", { family: "cosmos", mode: "send" }); |
| 50 | + |
| 51 | + expect(recallSignContext(signedOperation)?.earnTransactionType).toBeUndefined(); |
| 52 | + }); |
| 53 | + |
| 54 | + it("survives a read, so a rebroadcast still correlates", () => { |
| 55 | + const signedOperation = signed({ type: "DELEGATE" }); |
| 56 | + rememberSignContext(signedOperation, "cosmos", { family: "cosmos", mode: "delegate" }); |
| 57 | + |
| 58 | + recallSignContext(signedOperation); |
| 59 | + expect(hasSignContext(signedOperation)).toBe(true); |
| 60 | + }); |
| 61 | +}); |
| 62 | + |
| 63 | +describe("buildBroadcastCommonEvent with a sign context", () => { |
| 64 | + // Solana's stake.withdraw becomes an `IN` operation, indistinguishable from an incoming |
| 65 | + // transfer — correlation is the only thing that makes it reportable at all. |
| 66 | + it("recovers an action the operation type cannot express", () => { |
| 67 | + const solana = account("solana", "solana", "SOL"); |
| 68 | + const signedOperation = signed({ type: "IN", extra: {} }); |
| 69 | + rememberSignContext(signedOperation, "solana", { |
| 70 | + family: "solana", |
| 71 | + model: { kind: "stake.withdraw" }, |
| 72 | + }); |
| 73 | + |
| 74 | + expect(buildBroadcastCommonEvent({ ...attribution(solana), signedOperation })).toMatchObject({ |
| 75 | + earnTransactionType: "withdraw", |
| 76 | + rawTransactionType: "stake.withdraw", |
| 77 | + dataSource: TransactionDataSource.Sign, |
| 78 | + }); |
| 79 | + }); |
| 80 | + |
| 81 | + it("recovers the validator for a family that drops it from the operation", () => { |
| 82 | + const cardano = account("cardano", "cardano", "ADA"); |
| 83 | + const signedOperation = signed({ type: "DELEGATE", extra: {} }); |
| 84 | + rememberSignContext(signedOperation, "cardano", { |
| 85 | + family: "cardano", |
| 86 | + mode: "delegate", |
| 87 | + poolId: "pool123", |
| 88 | + }); |
| 89 | + |
| 90 | + expect( |
| 91 | + buildBroadcastCommonEvent({ ...attribution(cardano), signedOperation }).validators, |
| 92 | + ).toEqual(["pool123"]); |
| 93 | + }); |
| 94 | + |
| 95 | + // The sign stage has no broadcastConfig, so it must never win on attribution. |
| 96 | + it("keeps the broadcast stage's route attribution", () => { |
| 97 | + const cosmos = account("cosmos", "cosmos", "ATOM"); |
| 98 | + const signedOperation = signed({ type: "DELEGATE", extra: {} }); |
| 99 | + rememberSignContext(signedOperation, "cosmos", { family: "cosmos", mode: "delegate" }); |
| 100 | + |
| 101 | + const common = buildBroadcastCommonEvent({ |
| 102 | + account: cosmos, |
| 103 | + mainAccount: cosmos, |
| 104 | + flow: TransactionFlow.Dapp, |
| 105 | + manifestId: "kiln", |
| 106 | + signedOperation, |
| 107 | + }); |
| 108 | + |
| 109 | + expect(common).toMatchObject({ flow: TransactionFlow.Dapp, manifestId: "kiln" }); |
| 110 | + }); |
| 111 | + |
| 112 | + // Celo and tron only expose the target on the optimistic operation, so a correlation hit |
| 113 | + // must not discard it just because the sign stage had none. |
| 114 | + it("keeps an operation-only validator when the sign stage had none", () => { |
| 115 | + const celo = account("celo", "celo", "CELO"); |
| 116 | + const signedOperation = signed({ type: "VOTE", extra: { celoSourceValidator: "0xgroup" } }); |
| 117 | + rememberSignContext(signedOperation, "celo", { family: "celo", mode: "vote" }); |
| 118 | + |
| 119 | + const common = buildBroadcastCommonEvent({ ...attribution(celo), signedOperation }); |
| 120 | + expect(common).toMatchObject({ |
| 121 | + earnTransactionType: "delegate", |
| 122 | + dataSource: TransactionDataSource.Sign, |
| 123 | + validators: ["0xgroup"], |
| 124 | + }); |
| 125 | + }); |
| 126 | + |
| 127 | + it("falls back to the operation type when nothing correlates", () => { |
| 128 | + const cosmos = account("cosmos", "cosmos", "ATOM"); |
| 129 | + |
| 130 | + expect( |
| 131 | + buildBroadcastCommonEvent({ |
| 132 | + ...attribution(cosmos), |
| 133 | + signedOperation: signed({ type: "DELEGATE", extra: {} }), |
| 134 | + }), |
| 135 | + ).toMatchObject({ |
| 136 | + earnTransactionType: "delegate", |
| 137 | + rawTransactionType: "DELEGATE", |
| 138 | + dataSource: TransactionDataSource.Broadcast, |
| 139 | + }); |
| 140 | + }); |
| 141 | + |
| 142 | + it("falls back when the sign stage saw no staking action", () => { |
| 143 | + const cosmos = account("cosmos", "cosmos", "ATOM"); |
| 144 | + const signedOperation = signed({ type: "DELEGATE", extra: {} }); |
| 145 | + rememberSignContext(signedOperation, "cosmos", { family: "cosmos", mode: "send" }); |
| 146 | + |
| 147 | + expect(buildBroadcastCommonEvent({ ...attribution(cosmos), signedOperation })).toMatchObject({ |
| 148 | + dataSource: TransactionDataSource.Broadcast, |
| 149 | + }); |
| 150 | + }); |
| 151 | +}); |
0 commit comments