{"version":3,"file":"balance-flows.mjs","names":["data","gasCoins","inputs","coins","commands"],"sources":["../../../src/transaction-analyzer/rules/balance-flows.ts"],"sourcesContent":["// Copyright (c) Mysten Labs, Inc.\n// SPDX-License-Identifier: Apache-2.0\n\nimport { normalizeSuiAddress, normalizeStructTag } from '@mysten/sui/utils';\nimport type { TransactionAnalysisIssue } from '../analyzer.js';\nimport { createAnalyzer } from '../analyzer.js';\nimport { bcs } from '@mysten/sui/bcs';\nimport { commands } from './commands.js';\nimport type {\n\tAnalyzedCommand,\n\tAnalyzedCommandArgument,\n\tAnalyzedMoveCallCommand,\n} from './commands.js';\nimport { data } from './core.js';\nimport { inputs } from './inputs.js';\nimport { coins, gasCoins } from './coins.js';\n\nexport interface CoinFlow {\n\tcoinType: string;\n\tamount: bigint;\n}\n\n/**\n * Per-address signed balance deltas. Negative = value left the address on\n * net; positive = value arrived on net.\n *\n * `sender` is the transaction sender's signed flows (empty if the sender\n * didn't move any tracked value). `sponsor` is the gas payer's signed flows\n * when `gasData.owner` differs from `data.sender`, or `null` when the tx\n * isn't sponsored.\n */\nexport interface BalanceFlowsResult {\n\tbyAddress: Record<string, CoinFlow[]>;\n\tsender: CoinFlow[];\n\tsponsor: CoinFlow[] | null;\n}\n\nexport interface BalanceFlowsMoveCallHandlerContext {\n\tgetTrackedBalance: (ref: AnalyzedCommandArgument) => TrackedBalance | null;\n\t/**\n\t * Stable string key for a trackable argument — safe to use as a Map\n\t * key to correlate state across commands. Returns `null` for Pure /\n\t * Unknown / Withdrawal args.\n\t */\n\tkeyFor: (arg: AnalyzedCommandArgument) => string | null;\n\t/**\n\t * Stable key for this command's own output at `outputIndex`. Same\n\t * encoding as `keyFor` applied to a Result arg pointing at this slot.\n\t */\n\toutputKey: (outputIndex: number) => string;\n\t/** Register a tracked `Coin<T>` at this command's `outputIndex` result slot. */\n\ttrackCoinResult: (outputIndex: number, coinType: string, balance: bigint) => void;\n\t/** Register a tracked `Balance<T>` at this command's `outputIndex` result slot. */\n\ttrackBalanceResult: (outputIndex: number, coinType: string, balance: bigint) => void;\n\trecordFlow: (address: string | null, coinType: string, amount: bigint) => void;\n\taddIssue: (issue: TransactionAnalysisIssue) => void;\n}\n\n/** Return `true` to mark the MoveCall handled; `false` to pass through to subsequent handlers and the generic consume loop. */\nexport type BalanceFlowsMoveCallHandler = (\n\tcommand: AnalyzedMoveCallCommand,\n\tctx: BalanceFlowsMoveCallHandlerContext,\n) => boolean;\n\n/**\n * Factory a handler instance. Called once per `analyze()` run so the\n * returned handler can hold per-PTB state (e.g. tracking intermediate\n * results by command index) without leaking across runs.\n */\nexport type BalanceFlowsMoveCallHandlerFactory = () => BalanceFlowsMoveCallHandler;\n\nexport interface BalanceFlowsAnalyzerOptions {\n\t/** When true, the gas budget isn't charged against the gas payer's deltas. */\n\texcludeGasBudget?: boolean;\n\t/**\n\t * Handler factories for MoveCalls outside the built-in 0x2::coin /\n\t * 0x2::balance framework. Each factory is invoked per `analyze()` run,\n\t * and the resulting handlers are called in order for each MoveCall;\n\t * the first handler that returns `true` wins (suppresses later\n\t * handlers and the generic consume loop for that command).\n\t */\n\tmoveCallHandlers?: BalanceFlowsMoveCallHandlerFactory[];\n}\n\nconst SUI_FRAMEWORK = normalizeSuiAddress('0x2');\n\nexport const balanceFlows = createAnalyzer({\n\tdependencies: { data, commands, inputs, coins, gasCoins },\n\tanalyze:\n\t\t({ balanceFlows: opts = {} }: { balanceFlows?: BalanceFlowsAnalyzerOptions } = {}) =>\n\t\tasync ({ data, commands, inputs, coins, gasCoins }) => {\n\t\t\tconst { excludeGasBudget = false, moveCallHandlers = [] } = opts;\n\t\t\tconst issues: TransactionAnalysisIssue[] = [];\n\t\t\tconst trackedBalances = new Map<string, TrackedBalance>();\n\t\t\tconst redeemedWithdrawals = new Set<number>();\n\t\t\tconst deltas = new Map<string, Map<string, bigint>>();\n\n\t\t\tconst sender = data.sender ? normalizeSuiAddress(data.sender) : null;\n\t\t\tconst gasOwner = data.gasData.owner\n\t\t\t\t? normalizeSuiAddress(data.gasData.owner)\n\t\t\t\t: (sender ?? null);\n\n\t\t\tconst normalizeAddress = (address: string | null): string | null =>\n\t\t\t\taddress == null ? null : normalizeSuiAddress(address);\n\n\t\t\tconst recordFlow = (address: string | null, coinType: string, amount: bigint) => {\n\t\t\t\tif (!address || amount === 0n) return;\n\t\t\t\tlet byCoin = deltas.get(address);\n\t\t\t\tif (!byCoin) {\n\t\t\t\t\tbyCoin = new Map();\n\t\t\t\t\tdeltas.set(address, byCoin);\n\t\t\t\t}\n\t\t\t\tbyCoin.set(coinType, (byCoin.get(coinType) ?? 0n) + amount);\n\t\t\t};\n\n\t\t\tconst track = (key: string, coin: TrackedBalance) => {\n\t\t\t\ttrackedBalances.set(key, coin);\n\t\t\t};\n\n\t\t\tconst trackedBalanceKey = (ref: AnalyzedCommandArgument): string | null => {\n\t\t\t\tswitch (ref.$kind) {\n\t\t\t\t\tcase 'GasCoin':\n\t\t\t\t\t\treturn 'gas';\n\t\t\t\t\tcase 'Object':\n\t\t\t\t\t\treturn `input:${ref.index}`;\n\t\t\t\t\tcase 'Result':\n\t\t\t\t\t\treturn `result:${ref.index[0]},${ref.index[1]}`;\n\t\t\t\t\tcase 'Unknown':\n\t\t\t\t\tcase 'Pure':\n\t\t\t\t\tcase 'Withdrawal':\n\t\t\t\t\t\treturn null;\n\t\t\t\t\tdefault: {\n\t\t\t\t\t\tconst _exhaustive: never = ref;\n\t\t\t\t\t\tissues.push({\n\t\t\t\t\t\t\tmessage: `Unknown command argument kind: ${JSON.stringify(_exhaustive)}`,\n\t\t\t\t\t\t});\n\t\t\t\t\t\treturn null;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tconst getTrackedBalance = (ref: AnalyzedCommandArgument): TrackedBalance | null => {\n\t\t\t\tconst key = trackedBalanceKey(ref);\n\t\t\t\tconst coin = key ? trackedBalances.get(key) : null;\n\t\t\t\treturn coin && !coin.consumed ? coin : null;\n\t\t\t};\n\n\t\t\tconst splitCoin = (command: Extract<AnalyzedCommand, { $kind: 'SplitCoins' }>) => {\n\t\t\t\tconst coin = getTrackedBalance(command.coin);\n\t\t\t\tif (!coin) return;\n\n\t\t\t\tif (!command.amounts.every((a) => a.$kind === 'Pure')) {\n\t\t\t\t\tcoin.consume();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst amounts = command.amounts.map((a) => {\n\t\t\t\t\tif (a.$kind !== 'Pure') throw new Error('Expected pure value');\n\t\t\t\t\treturn BigInt(bcs.u64().fromBase64(a.bytes));\n\t\t\t\t});\n\n\t\t\t\tconst total = amounts.reduce((a, b) => a + b, 0n);\n\t\t\t\twithdrawOrFlag(coin, total, `SplitCoins at command ${command.index}`);\n\n\t\t\t\tamounts.forEach((amount, i) => {\n\t\t\t\t\ttrack(\n\t\t\t\t\t\t`result:${command.index},${i}`,\n\t\t\t\t\t\tnew TrackedBalance('coin', coin.coinType, amount, coin.ownerAddress),\n\t\t\t\t\t);\n\t\t\t\t});\n\t\t\t};\n\n\t\t\tconst withdrawOrFlag = (coin: TrackedBalance, amount: bigint, context: string) => {\n\t\t\t\tif (amount > coin.balance) {\n\t\t\t\t\tissues.push({\n\t\t\t\t\t\tmessage: `${context} takes ${amount} from a ${coin.coinType} coin with only ${coin.balance} available`,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tcoin.withdraw(amount);\n\t\t\t};\n\n\t\t\tconst mergeCoins = (command: Extract<AnalyzedCommand, { $kind: 'MergeCoins' }>) => {\n\t\t\t\tconst sources = command.sources.map(getTrackedBalance);\n\t\t\t\tconst amount = sources.reduce((a, c) => a + (c?.balance ?? 0n), 0n);\n\t\t\t\tfor (const src of sources) src?.consume();\n\t\t\t\tconst dest = getTrackedBalance(command.destination);\n\t\t\t\tif (!dest) return;\n\t\t\t\tdest.balance += amount;\n\t\t\t};\n\n\t\t\tconst transferObjects = (command: Extract<AnalyzedCommand, { $kind: 'TransferObjects' }>) => {\n\t\t\t\tconst address =\n\t\t\t\t\tcommand.address.$kind === 'Pure' ? bcs.Address.fromBase64(command.address.bytes) : null;\n\t\t\t\tconst dest = normalizeAddress(address);\n\t\t\t\tfor (const obj of command.objects) {\n\t\t\t\t\tconst tracked = getTrackedBalance(obj);\n\t\t\t\t\tif (tracked) {\n\t\t\t\t\t\trecordFlow(dest, tracked.coinType, tracked.balance);\n\t\t\t\t\t\ttracked.consume();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tconst getCoinTypeFromTypeArgs = (command: AnalyzedMoveCallCommand): string | null => {\n\t\t\t\tconst typeArg = command.command.typeArguments?.[0];\n\t\t\t\treturn typeArg ? normalizeStructTag(typeArg) : null;\n\t\t\t};\n\n\t\t\tconst handleFrameworkMoveCall = (command: AnalyzedMoveCallCommand): boolean => {\n\t\t\t\tconst pkg = normalizeSuiAddress(command.command.package);\n\t\t\t\tif (pkg !== SUI_FRAMEWORK) return false;\n\n\t\t\t\tconst mod = command.command.module;\n\t\t\t\tconst fn = command.command.function;\n\t\t\t\tconst coinType = getCoinTypeFromTypeArgs(command);\n\n\t\t\t\tconst allowanceSpend =\n\t\t\t\t\tmod === 'allowance' && (fn === 'balance_spend' || fn === 'app_balance_spend');\n\t\t\t\tif (allowanceSpend || (fn === 'redeem_funds' && (mod === 'coin' || mod === 'balance'))) {\n\t\t\t\t\tconst withdrawalIndex = allowanceSpend ? (fn === 'app_balance_spend' ? 2 : 1) : 0;\n\t\t\t\t\tconst arg = command.arguments[withdrawalIndex];\n\t\t\t\t\tif (arg?.$kind !== 'Withdrawal') {\n\t\t\t\t\t\tissues.push({\n\t\t\t\t\t\t\tmessage: `${mod}::${fn} at command ${command.index} expects a FundsWithdrawal input but got ${arg?.$kind ?? 'none'}`,\n\t\t\t\t\t\t});\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t\tlet ownerRaw: string | null;\n\t\t\t\t\tif (arg.withdrawFrom === 'Sender') {\n\t\t\t\t\t\townerRaw = sender ?? null;\n\t\t\t\t\t} else if (arg.withdrawFrom === 'SenderAllowance') {\n\t\t\t\t\t\t// The allowance's funder is debited, not the sender or the gas owner.\n\t\t\t\t\t\townerRaw = arg.funder;\n\t\t\t\t\t} else if (data.gasData.owner) {\n\t\t\t\t\t\townerRaw = gasOwner;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tissues.push({\n\t\t\t\t\t\t\tmessage: `${mod}::${fn} at command ${command.index} withdraws from Sponsor but the transaction has no gas owner`,\n\t\t\t\t\t\t});\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t\tredeemedWithdrawals.add(arg.index);\n\t\t\t\t\tconst owner = normalizeAddress(ownerRaw);\n\t\t\t\t\ttrack(\n\t\t\t\t\t\t`result:${command.index},0`,\n\t\t\t\t\t\tnew TrackedBalance(\n\t\t\t\t\t\t\tmod === 'coin' ? 'coin' : 'balance',\n\t\t\t\t\t\t\targ.coinType,\n\t\t\t\t\t\t\targ.amount,\n\t\t\t\t\t\t\towner,\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\t\trecordFlow(owner, arg.coinType, -arg.amount);\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\n\t\t\t\tif (fn === 'send_funds' && (mod === 'coin' || mod === 'balance')) {\n\t\t\t\t\tif (command.arguments.length < 2) return false;\n\t\t\t\t\tconst tracked = getTrackedBalance(command.arguments[0]);\n\t\t\t\t\tconst addrArg = command.arguments[1];\n\t\t\t\t\tconst destAddress =\n\t\t\t\t\t\taddrArg.$kind === 'Pure' ? bcs.Address.fromBase64(addrArg.bytes) : null;\n\t\t\t\t\tif (tracked) {\n\t\t\t\t\t\trecordFlow(normalizeAddress(destAddress), tracked.coinType, tracked.balance);\n\t\t\t\t\t\ttracked.consume();\n\t\t\t\t\t}\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\n\t\t\t\tif (\n\t\t\t\t\t(fn === 'into_balance' && mod === 'coin') ||\n\t\t\t\t\t(fn === 'from_balance' && mod === 'coin')\n\t\t\t\t) {\n\t\t\t\t\tif (command.arguments.length < 1) return false;\n\t\t\t\t\tconst source = getTrackedBalance(command.arguments[0]);\n\t\t\t\t\tif (source) {\n\t\t\t\t\t\tconst resultKind = fn === 'into_balance' ? 'balance' : 'coin';\n\t\t\t\t\t\ttrack(\n\t\t\t\t\t\t\t`result:${command.index},0`,\n\t\t\t\t\t\t\tnew TrackedBalance(resultKind, source.coinType, source.balance, source.ownerAddress),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tsource.consume();\n\t\t\t\t\t}\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\n\t\t\t\tif (\n\t\t\t\t\t(fn === 'split' && (mod === 'coin' || mod === 'balance')) ||\n\t\t\t\t\t(fn === 'take' && mod === 'coin')\n\t\t\t\t) {\n\t\t\t\t\tif (command.arguments.length < 2) return false;\n\t\t\t\t\tconst source = getTrackedBalance(command.arguments[0]);\n\t\t\t\t\tif (!source) return true;\n\n\t\t\t\t\tconst amountArg = command.arguments[1];\n\t\t\t\t\tif (amountArg.$kind !== 'Pure') {\n\t\t\t\t\t\tsource.consume();\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst amount = BigInt(bcs.u64().fromBase64(amountArg.bytes));\n\t\t\t\t\twithdrawOrFlag(source, amount, `${mod}::${fn} at command ${command.index}`);\n\t\t\t\t\t// `coin::take` returns Coin<T>; `split` matches the invoked module.\n\t\t\t\t\tconst resultKind = fn === 'take' ? 'coin' : mod;\n\t\t\t\t\ttrack(\n\t\t\t\t\t\t`result:${command.index},0`,\n\t\t\t\t\t\tnew TrackedBalance(resultKind, source.coinType, amount, source.ownerAddress),\n\t\t\t\t\t);\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\n\t\t\t\tif (fn === 'withdraw_all' && mod === 'balance') {\n\t\t\t\t\tif (command.arguments.length < 1) return false;\n\t\t\t\t\tconst source = getTrackedBalance(command.arguments[0]);\n\t\t\t\t\tif (source) {\n\t\t\t\t\t\ttrack(\n\t\t\t\t\t\t\t`result:${command.index},0`,\n\t\t\t\t\t\t\tnew TrackedBalance('balance', source.coinType, source.balance, source.ownerAddress),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tsource.balance = 0n;\n\t\t\t\t\t}\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\n\t\t\t\tif (\n\t\t\t\t\t(fn === 'join' && (mod === 'coin' || mod === 'balance')) ||\n\t\t\t\t\t(fn === 'put' && mod === 'coin')\n\t\t\t\t) {\n\t\t\t\t\tif (command.arguments.length < 2) return false;\n\t\t\t\t\tconst dest = getTrackedBalance(command.arguments[0]);\n\t\t\t\t\tconst source = getTrackedBalance(command.arguments[1]);\n\t\t\t\t\tif (!source) return true;\n\t\t\t\t\tif (dest) dest.balance += source.balance;\n\t\t\t\t\tsource.consume();\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\n\t\t\t\tif (fn === 'zero' && (mod === 'coin' || mod === 'balance')) {\n\t\t\t\t\tif (coinType) {\n\t\t\t\t\t\ttrack(`result:${command.index},0`, new TrackedBalance(mod, coinType, 0n, null));\n\t\t\t\t\t}\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\n\t\t\t\treturn false;\n\t\t\t};\n\n\t\t\tconst suiType = normalizeStructTag('0x2::sui::SUI');\n\t\t\tconst normalizedGasOwner = normalizeAddress(gasOwner);\n\t\t\tconst paymentIsEmpty = (data.gasData.payment?.length ?? 0) === 0;\n\n\t\t\t// Gas payment coins are smashed into a single tx.gas coin.\n\t\t\tconst gasBalance = gasCoins.reduce((a, c) => a + c.balance, 0n);\n\t\t\tconst gasCoin = new TrackedBalance('coin', suiType, gasBalance, normalizedGasOwner);\n\t\t\ttrack('gas', gasCoin);\n\t\t\trecordFlow(normalizedGasOwner, suiType, -gasBalance);\n\n\t\t\tif (data.gasData.budget) {\n\t\t\t\tif (!excludeGasBudget) {\n\t\t\t\t\tconst budget = BigInt(data.gasData.budget);\n\t\t\t\t\tif (paymentIsEmpty) {\n\t\t\t\t\t\trecordFlow(normalizedGasOwner, suiType, -budget);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif (budget > gasBalance) {\n\t\t\t\t\t\t\tissues.push({\n\t\t\t\t\t\t\t\tmessage: `Gas budget ${budget} exceeds the gas payment balance ${gasBalance}`,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t\tgasCoin.balance -= budget;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (!excludeGasBudget) {\n\t\t\t\tissues.push({ message: 'Gas budget not set in Transaction' });\n\t\t\t}\n\n\t\t\tfor (const input of inputs) {\n\t\t\t\tif (input.$kind === 'Object' && coins[input.object.objectId]) {\n\t\t\t\t\tconst coin = coins[input.object.objectId];\n\t\t\t\t\tconst tc = new TrackedBalance(\n\t\t\t\t\t\t'coin',\n\t\t\t\t\t\tcoin.coinType,\n\t\t\t\t\t\tcoin.balance,\n\t\t\t\t\t\tnormalizeAddress(coin.ownerAddress),\n\t\t\t\t\t);\n\t\t\t\t\ttrack(`input:${input.index}`, tc);\n\t\t\t\t\trecordFlow(tc.ownerAddress, tc.coinType, -tc.balance);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst handlers = moveCallHandlers.map((factory) => factory());\n\t\t\tconst buildHandlerContext = (\n\t\t\t\tcommand: AnalyzedMoveCallCommand,\n\t\t\t): BalanceFlowsMoveCallHandlerContext => ({\n\t\t\t\tgetTrackedBalance,\n\t\t\t\tkeyFor: trackedBalanceKey,\n\t\t\t\toutputKey: (outputIndex) => `result:${command.index},${outputIndex}`,\n\t\t\t\ttrackCoinResult: (outputIndex, coinType, balance) =>\n\t\t\t\t\ttrack(\n\t\t\t\t\t\t`result:${command.index},${outputIndex}`,\n\t\t\t\t\t\tnew TrackedBalance('coin', coinType, balance, null),\n\t\t\t\t\t),\n\t\t\t\ttrackBalanceResult: (outputIndex, coinType, balance) =>\n\t\t\t\t\ttrack(\n\t\t\t\t\t\t`result:${command.index},${outputIndex}`,\n\t\t\t\t\t\tnew TrackedBalance('balance', coinType, balance, null),\n\t\t\t\t\t),\n\t\t\t\trecordFlow,\n\t\t\t\taddIssue: (issue) => issues.push(issue),\n\t\t\t});\n\n\t\t\tfor (const command of commands) {\n\t\t\t\tswitch (command.$kind) {\n\t\t\t\t\tcase 'SplitCoins':\n\t\t\t\t\t\tsplitCoin(command);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 'MergeCoins':\n\t\t\t\t\t\tmergeCoins(command);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 'TransferObjects':\n\t\t\t\t\t\ttransferObjects(command);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 'MakeMoveVec':\n\t\t\t\t\t\tcommand.elements.forEach((el) => {\n\t\t\t\t\t\t\tgetTrackedBalance(el)?.consume();\n\t\t\t\t\t\t});\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 'MoveCall': {\n\t\t\t\t\t\tif (handleFrameworkMoveCall(command)) break;\n\t\t\t\t\t\tconst ctx = buildHandlerContext(command);\n\t\t\t\t\t\tif (handlers.some((handler) => handler(command, ctx))) break;\n\t\t\t\t\t\tcommand.arguments.forEach((arg) => {\n\t\t\t\t\t\t\tif (arg.accessLevel === 'read') return;\n\t\t\t\t\t\t\tgetTrackedBalance(arg)?.consume();\n\t\t\t\t\t\t});\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tcase 'Upgrade':\n\t\t\t\t\tcase 'Publish':\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tissues.push({\n\t\t\t\t\t\t\tmessage: `Unsupported command type: ${(command as { $kind: string }).$kind}`,\n\t\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Opaque Move calls can redeem withdrawals internally. Do not report zero\n\t\t\t// outflow when the reservation's spend could not be accounted for.\n\t\t\tfor (const input of inputs) {\n\t\t\t\tif (input.$kind === 'Withdrawal' && !redeemedWithdrawals.has(input.index)) {\n\t\t\t\t\tissues.push({ message: `Cannot track redemption of withdrawal input ${input.index}` });\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (issues.length) return { issues };\n\n\t\t\t// Implicit return: still-alive tracked coins credit their owner.\n\t\t\tfor (const [, coin] of trackedBalances) {\n\t\t\t\tif (coin.balance <= 0n) continue;\n\t\t\t\trecordFlow(coin.ownerAddress, coin.coinType, coin.balance);\n\t\t\t}\n\n\t\t\tconst byAddress: Record<string, CoinFlow[]> = {};\n\t\t\tfor (const [address, byCoin] of deltas) {\n\t\t\t\tbyAddress[address] = Array.from(byCoin, ([coinType, amount]) => ({ coinType, amount }));\n\t\t\t}\n\n\t\t\tconst senderFlows = sender ? (byAddress[sender] ?? []) : [];\n\t\t\tconst sponsorAddr =\n\t\t\t\tnormalizedGasOwner && normalizedGasOwner !== sender ? normalizedGasOwner : null;\n\t\t\tconst sponsorFlows = sponsorAddr ? (byAddress[sponsorAddr] ?? []) : null;\n\n\t\t\treturn {\n\t\t\t\tresult: {\n\t\t\t\t\tbyAddress,\n\t\t\t\t\tsender: senderFlows,\n\t\t\t\t\tsponsor: sponsorFlows,\n\t\t\t\t} satisfies BalanceFlowsResult,\n\t\t\t};\n\t\t},\n});\n\nexport class TrackedBalance {\n\tkind: 'coin' | 'balance';\n\tcoinType: string;\n\tbalance: bigint;\n\townerAddress: string | null;\n\tconsumed = false;\n\n\tconstructor(\n\t\tkind: 'coin' | 'balance',\n\t\tcoinType: string,\n\t\tbalance: bigint,\n\t\townerAddress: string | null,\n\t) {\n\t\tthis.kind = kind;\n\t\tthis.coinType = coinType;\n\t\tthis.balance = balance;\n\t\tthis.ownerAddress = ownerAddress;\n\t}\n\n\tconsume() {\n\t\tthis.balance = 0n;\n\t\tthis.ownerAddress = null;\n\t\tthis.consumed = true;\n\t}\n\n\twithdraw(amount: bigint): void {\n\t\tthis.balance -= amount;\n\t}\n}\n"],"mappings":"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"}