taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 8b5e3eb7e42cfb4d33d7a72b506266515196a42f
parent ca45107eca3e2c35c018826edec09546934eb802
Author: Florian Dold <florian@dold.me>
Date:   Wed,  3 Dec 2025 15:42:41 +0100

wallet-core: return txState in p2p prepare requests

Also refactor some state transitions

Diffstat:
Mpackages/taler-util/src/types-taler-wallet.ts | 10++++++++++
Mpackages/taler-wallet-core/src/pay-peer-common.ts | 39---------------------------------------
Mpackages/taler-wallet-core/src/pay-peer-pull-credit.ts | 81+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Mpackages/taler-wallet-core/src/pay-peer-pull-debit.ts | 85++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Mpackages/taler-wallet-core/src/pay-peer-push-credit.ts | 99++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Mpackages/taler-wallet-core/src/pay-peer-push-debit.ts | 203+++++++++++++++++++++++++++++++++++++++++--------------------------------------
6 files changed, 279 insertions(+), 238 deletions(-)

diff --git a/packages/taler-util/src/types-taler-wallet.ts b/packages/taler-util/src/types-taler-wallet.ts @@ -3683,6 +3683,11 @@ export interface PreparePeerPushCreditResponse { transactionId: TransactionIdStr; + /** + * State of the existing or newly created transaction. + */ + txState: TransactionState; + exchangeBaseUrl: string; scopeInfo: ScopeInfo; @@ -3701,6 +3706,11 @@ export interface PreparePeerPullDebitResponse { transactionId: TransactionIdStr; + /** + * State of the existing or newly created transaction. + */ + txState: TransactionState; + exchangeBaseUrl: string; scopeInfo: ScopeInfo; diff --git a/packages/taler-wallet-core/src/pay-peer-common.ts b/packages/taler-wallet-core/src/pay-peer-common.ts @@ -204,45 +204,6 @@ interface RecordCtx<Store extends WalletDbStoresName> { }; } -/** Create a new record, update its metadata and notify its creation */ -export async function recordCreate< - Store extends WalletDbStoresName, - ExtraStores extends WalletDbStoresArr = [], ->( - ctx: RecordCtx<Store>, - opts: { extraStores?: ExtraStores; label?: string }, - lambda: ( - tx: WalletDbReadWriteTransaction< - [Store, "transactionsMeta", ...ExtraStores] - >, - ) => Promise<StoreType<Store>>, -) { - const baseStore = [ctx.store, "transactionsMeta" as const]; - const storeNames = opts.extraStores - ? [...baseStore, ...opts.extraStores] - : baseStore; - await ctx.wex.db.runReadWriteTx( - { storeNames, label: opts.label }, - async (tx) => { - const oldTxState: TransactionState = { - major: TransactionMajorState.None, - }; - const rec = await lambda(tx); - // FIXME: DbReadWriteTransaction conditional type confuse Typescript, we should simplify invariable - await (tx[ctx.store] as any).add(rec); - await tx.transactionsMeta.put(ctx.recordMeta(rec)); - const newTxState = ctx.recordState(rec); - applyNotifyTransition(tx.notify, ctx.transactionId, { - oldTxState, - newTxState: newTxState.txState, - balanceEffect: BalanceEffect.Any, - oldStId: 0, - newStId: newTxState.stId, - }); - }, - ); -} - /** * Optionally update an existing record, ignore if missing. * If a transition occurs, update its metadata and notify. diff --git a/packages/taler-wallet-core/src/pay-peer-pull-credit.ts b/packages/taler-wallet-core/src/pay-peer-pull-credit.ts @@ -55,6 +55,7 @@ import { } from "@gnu-taler/taler-util"; import { PendingTaskType, + RecordHandle, TaskIdStr, TaskIdentifiers, TaskRunResult, @@ -62,6 +63,7 @@ import { TransitionResultType, constructTaskIdentifier, genericWaitForStateVal, + getGenericRecordHandle, requireExchangeTosAcceptedOrThrow, runWithClientCancellation, } from "./common.js"; @@ -94,7 +96,6 @@ import { import { getMergeReserveInfo, isPurseDeposited, - recordCreate, recordDelete, recordTransition, recordTransitionStatus, @@ -304,6 +305,25 @@ export class PeerPullCreditTransactionContext implements TransactionContext { }; } + async getRecordHandle( + tx: WalletDbReadWriteTransaction<["peerPullCredit", "transactionsMeta"]>, + ): Promise< + [PeerPullCreditRecord | undefined, RecordHandle<PeerPullCreditRecord>] + > { + return getGenericRecordHandle<PeerPullCreditRecord>( + this, + tx as any, + async () => tx.peerPullCredit.get(this.pursePub), + async (r) => { + await tx.peerPullCredit.put(r); + }, + async () => tx.peerPullCredit.delete(this.pursePub), + (r) => computePeerPullCreditTransactionState(r), + (r) => r.status, + () => this.updateTransactionMeta(tx), + ); + } + async deleteTransaction(): Promise<void> { const res = await this.wex.db.runReadWriteTx( { @@ -1100,36 +1120,35 @@ export async function initiatePeerPullPayment( const mergeTimestamp = TalerPreciseTimestamp.now(); const ctx = new PeerPullCreditTransactionContext(wex, pursePair.pub); - await recordCreate( - ctx, - { - extraStores: ["contractTerms"], - label: "create-transaction-peer-pull-credit", - }, - async (tx) => { - await tx.contractTerms.put({ - contractTermsRaw: contractTerms, - h: hContractTerms, - }); - return { - amount: req.partialContractTerms.amount, - contractTermsHash: hContractTerms, - exchangeBaseUrl: exchangeBaseUrl, - pursePriv: pursePair.priv, - pursePub: pursePair.pub, - mergePriv: mergePair.priv, - mergePub: mergePair.pub, - status: PeerPullPaymentCreditStatus.PendingCreatePurse, - mergeTimestamp: timestampPreciseToDb(mergeTimestamp), - contractEncNonce, - mergeReserveRowId: mergeReserveRowId, - contractPriv: contractKeyPair.priv, - contractPub: contractKeyPair.pub, - withdrawalGroupId, - estimatedAmountEffective: wi.withdrawalAmountEffective, - }; - }, - ); + + await wex.db.runAllStoresReadWriteTx({}, async (tx) => { + await tx.contractTerms.put({ + contractTermsRaw: contractTerms, + h: hContractTerms, + }); + const [oldRec, h] = await ctx.getRecordHandle(tx); + if (oldRec) { + throw Error("peer-pull-credit record already exists"); + } + await h.update({ + amount: req.partialContractTerms.amount, + contractTermsHash: hContractTerms, + exchangeBaseUrl: exchangeBaseUrl, + pursePriv: pursePair.priv, + pursePub: pursePair.pub, + mergePriv: mergePair.priv, + mergePub: mergePair.pub, + status: PeerPullPaymentCreditStatus.PendingCreatePurse, + mergeTimestamp: timestampPreciseToDb(mergeTimestamp), + contractEncNonce, + mergeReserveRowId: mergeReserveRowId, + contractPriv: contractKeyPair.priv, + contractPub: contractKeyPair.pub, + withdrawalGroupId, + estimatedAmountEffective: wi.withdrawalAmountEffective, + }); + }); + wex.taskScheduler.startShepherdTask(ctx.taskId); return { diff --git a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts @@ -64,11 +64,13 @@ import { import { PreviousPayCoins, selectPeerCoins } from "./coinSelection.js"; import { PendingTaskType, + RecordHandle, TaskIdStr, TaskRunResult, TransactionContext, TransitionResultType, constructTaskIdentifier, + getGenericRecordHandle, spendCoins, } from "./common.js"; import { @@ -85,7 +87,6 @@ import { getTotalPeerPaymentCost, isPurseDeposited, queryCoinInfosForSelection, - recordCreate, recordDelete, recordTransition, recordTransitionStatus, @@ -135,6 +136,7 @@ export class PeerPullDebitTransactionContext implements TransactionContext { currency: Amounts.currencyOf(rec.amount), exchanges: [rec.exchangeBaseUrl], }); + updateTransactionMeta = ( tx: WalletDbReadWriteTransaction<["peerPullDebit", "transactionsMeta"]>, ) => recordUpdateMeta(this, tx); @@ -183,6 +185,28 @@ export class PeerPullDebitTransactionContext implements TransactionContext { }; } + async getRecordHandle( + tx: WalletDbReadWriteTransaction<["peerPullDebit", "transactionsMeta"]>, + ): Promise< + [ + PeerPullPaymentIncomingRecord | undefined, + RecordHandle<PeerPullPaymentIncomingRecord>, + ] + > { + return getGenericRecordHandle<PeerPullPaymentIncomingRecord>( + this, + tx as any, + async () => tx.peerPullDebit.get(this.peerPullDebitId), + async (r) => { + await tx.peerPullDebit.put(r); + }, + async () => tx.peerPullDebit.delete(this.peerPullDebitId), + (r) => computePeerPullDebitTransactionState(r), + (r) => r.status, + () => this.updateTransactionMeta(tx), + ); + } + async deleteTransaction(): Promise<void> { const res = await this.wex.db.runReadWriteTx( { storeNames: ["peerPullDebit", "transactionsMeta"] }, @@ -829,6 +853,7 @@ export async function preparePeerPullDebit( contractTerms, scopeInfo, exchangeBaseUrl: peerPullDebitRecord.exchangeBaseUrl, + rec: peerPullDebitRecord, }; }, ); @@ -841,6 +866,7 @@ export async function preparePeerPullDebit( contractTerms: existing.contractTerms.contractTermsRaw, scopeInfo: existing.scopeInfo, exchangeBaseUrl: existing.exchangeBaseUrl, + txState: computePeerPullDebitTransactionState(existing.rec), transactionId: constructTransactionIdentifier({ tag: TransactionType.PeerPullDebit, peerPullDebitId: existing.peerPullDebitRecord.peerPullDebitId, @@ -948,42 +974,43 @@ export async function preparePeerPullDebit( const totalAmount = await getTotalPeerPaymentCost(wex, coins); const ctx = new PeerPullDebitTransactionContext(wex, peerPullDebitId); - await recordCreate( - ctx, - { - extraStores: ["contractTerms"], - label: "create-transaction-peer-pull-credit", - }, - async (tx) => { - await tx.contractTerms.put({ - h: contractTermsHash, - contractTermsRaw: contractTerms, - }); - return { - peerPullDebitId, - contractPriv: contractPriv, - exchangeBaseUrl: exchangeBaseUrl, - pursePub: pursePub, - timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()), - contractTermsHash, - amount: contractTerms.amount, - status: PeerPullDebitRecordStatus.DialogProposed, - totalCostEstimated: Amounts.stringify(totalAmount), - }; - }, - ); - wex.taskScheduler.startShepherdTask(ctx.taskId); - const scopeInfo = await wex.db.runAllStoresReadOnlyTx({}, (tx) => { - return getExchangeScopeInfo(tx, exchangeBaseUrl, currency); + const ret = await wex.db.runAllStoresReadWriteTx({}, async (tx) => { + await tx.contractTerms.put({ + h: contractTermsHash, + contractTermsRaw: contractTerms, + }); + const [rec, h] = await ctx.getRecordHandle(tx); + if (rec) { + throw Error("peer-pull-debit record already exists"); + } + const newRec: PeerPullPaymentIncomingRecord = { + peerPullDebitId, + contractPriv: contractPriv, + exchangeBaseUrl: exchangeBaseUrl, + pursePub: pursePub, + timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()), + contractTermsHash, + amount: contractTerms.amount, + status: PeerPullDebitRecordStatus.DialogProposed, + totalCostEstimated: Amounts.stringify(totalAmount), + }; + await h.update(newRec); + return { + newRec, + scopeInfo: await getExchangeScopeInfo(tx, exchangeBaseUrl, currency), + }; }); + wex.taskScheduler.startShepherdTask(ctx.taskId); + return { amount: contractTerms.amount, amountEffective: Amounts.stringify(totalAmount), amountRaw: contractTerms.amount, contractTerms: contractTerms, - scopeInfo, + scopeInfo: ret.scopeInfo, + txState: computePeerPullDebitTransactionState(ret.newRec), exchangeBaseUrl, transactionId: ctx.transactionId, }; diff --git a/packages/taler-wallet-core/src/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/pay-peer-push-credit.ts @@ -54,6 +54,7 @@ import { } from "@gnu-taler/taler-util"; import { PendingTaskType, + RecordHandle, TaskIdStr, TaskIdentifiers, TaskRunResult, @@ -61,6 +62,7 @@ import { TransitionResultType, constructTaskIdentifier, genericWaitForStateVal, + getGenericRecordHandle, requireExchangeTosAcceptedOrThrow, } from "./common.js"; import { @@ -93,7 +95,6 @@ import { import { getMergeReserveInfo, isPurseMerged, - recordCreate, recordDelete, recordTransition, recordTransitionStatus, @@ -268,6 +269,28 @@ export class PeerPushCreditTransactionContext implements TransactionContext { }; } + async getRecordHandle( + tx: WalletDbReadWriteTransaction<["peerPushCredit", "transactionsMeta"]>, + ): Promise< + [ + PeerPushPaymentIncomingRecord | undefined, + RecordHandle<PeerPushPaymentIncomingRecord>, + ] + > { + return getGenericRecordHandle<PeerPushPaymentIncomingRecord>( + this, + tx as any, + async () => tx.peerPushCredit.get(this.peerPushCreditId), + async (r) => { + await tx.peerPushCredit.put(r); + }, + async () => tx.peerPushCredit.delete(this.peerPushCreditId), + (r) => computePeerPushCreditTransactionState(r), + (r) => r.status, + () => this.updateTransactionMeta(tx), + ); + } + async deleteTransaction(): Promise<void> { const res = await this.wex.db.runReadWriteTx( { @@ -494,6 +517,7 @@ export async function preparePeerPushCredit( tag: TransactionType.PeerPushCredit, peerPushCreditId: existing.existingPushInc.peerPushCreditId, }), + txState: computePeerPushCreditTransactionState(existing.existingPushInc), scopeInfo, exchangeBaseUrl, ...getPeerCreditLimitInfo( @@ -569,48 +593,40 @@ export async function preparePeerPushCredit( } const ctx = new PeerPushCreditTransactionContext(wex, peerPushCreditId); - await recordCreate( - ctx, - { - extraStores: ["contractTerms"], - }, - async (tx) => { - await tx.contractTerms.put({ - h: contractTermsHash, - contractTermsRaw: dec.contractTerms, - }); - return { - peerPushCreditId, - contractPriv: contractPriv, - exchangeBaseUrl: exchangeBaseUrl, - mergePriv: dec.mergePriv, - pursePub: pursePub, - timestamp: timestampPreciseToDb(TalerPreciseTimestamp.now()), - contractTermsHash, - status: PeerPushCreditStatus.DialogProposed, - withdrawalGroupId, - currency: Amounts.currencyOf(purseStatus.balance), - estimatedAmountEffective: Amounts.stringify( - wi.withdrawalAmountEffective, - ), - }; - }, - ); + + const res = await wex.db.runAllStoresReadWriteTx({}, async (tx) => { + await tx.contractTerms.put({ + h: contractTermsHash, + contractTermsRaw: dec.contractTerms, + }); + + const [rec, h] = await ctx.getRecordHandle(tx); + if (rec) { + throw Error("record already exists"); + } + const newRec: PeerPushPaymentIncomingRecord = { + peerPushCreditId, + contractPriv: contractPriv, + exchangeBaseUrl: exchangeBaseUrl, + mergePriv: dec.mergePriv, + pursePub: pursePub, + timestamp: timestampPreciseToDb(TalerPreciseTimestamp.now()), + contractTermsHash, + status: PeerPushCreditStatus.DialogProposed, + withdrawalGroupId, + currency: Amounts.currencyOf(purseStatus.balance), + estimatedAmountEffective: Amounts.stringify(wi.withdrawalAmountEffective), + }; + await h.update(newRec); + return { + scopeInfo: await getExchangeScopeInfo(tx, exchangeBaseUrl, currency), + newRec, + }; + }); + wex.taskScheduler.startShepherdTask(ctx.taskId); const currency = Amounts.currencyOf(wi.withdrawalAmountRaw); - const scopeInfo = await wex.db.runReadOnlyTx( - { - storeNames: [ - "exchanges", - "exchangeDetails", - "globalCurrencyExchanges", - "globalCurrencyAuditors", - ], - }, - (tx) => getExchangeScopeInfo(tx, exchangeBaseUrl, currency), - ); - return { amount: purseStatus.balance, amountEffective: wi.withdrawalAmountEffective, @@ -618,7 +634,8 @@ export async function preparePeerPushCredit( contractTerms: dec.contractTerms, transactionId: ctx.transactionId, exchangeBaseUrl, - scopeInfo, + scopeInfo: res.scopeInfo, + txState: computePeerPushCreditTransactionState(res.newRec), ...getPeerCreditLimitInfo(exchange, purseStatus.balance), }; } diff --git a/packages/taler-wallet-core/src/pay-peer-push-debit.ts b/packages/taler-wallet-core/src/pay-peer-push-debit.ts @@ -59,11 +59,13 @@ import { } from "./coinSelection.js"; import { PendingTaskType, + RecordHandle, TaskIdStr, TaskRunResult, TransactionContext, TransitionResultType, constructTaskIdentifier, + getGenericRecordHandle, runWithClientCancellation, spendCoins, } from "./common.js"; @@ -87,7 +89,6 @@ import { getTotalPeerPaymentCostInTx, isPurseMerged, queryCoinInfosForSelection, - recordCreate, recordDelete, recordTransition, recordTransitionStatus, @@ -199,6 +200,25 @@ export class PeerPushDebitTransactionContext implements TransactionContext { }; } + async getRecordHandle( + tx: WalletDbReadWriteTransaction<["peerPushDebit", "transactionsMeta"]>, + ): Promise< + [PeerPushDebitRecord | undefined, RecordHandle<PeerPushDebitRecord>] + > { + return getGenericRecordHandle<PeerPushDebitRecord>( + this, + tx as any, + async () => tx.peerPushDebit.get(this.pursePub), + async (r) => { + await tx.peerPushDebit.put(r); + }, + async () => tx.peerPushDebit.delete(this.pursePub), + (r) => computePeerPushDebitTransactionState(r), + (r) => r.status, + () => this.updateTransactionMeta(tx), + ); + } + async deleteTransaction(): Promise<void> { const res = await this.wex.db.runReadWriteTx( { storeNames: ["peerPushDebit", "transactionsMeta"] }, @@ -975,111 +995,98 @@ export async function initiatePeerPushDebit( await updateWithdrawalDenomsForCurrency(wex, instructedAmount.currency); let exchangeBaseUrl; - await recordCreate( - ctx, - { - extraStores: [ - "coinAvailability", - "coinHistory", - "coins", - "contractTerms", - "denominations", - "exchangeDetails", - "exchanges", - "refreshGroups", - "refreshSessions", - "globalCurrencyExchanges", - "globalCurrencyAuditors", - ], - }, - async (tx) => { - const coinSelRes = await selectPeerCoinsInTx(wex, tx, { - instructedAmount, - // Any (single!) exchange that is in scope works. - restrictScope: req.restrictScope, - feesCoveredByCounterparty: false, - }); - let coins: SelectedProspectiveCoin[] | undefined = undefined; + await wex.db.runAllStoresReadWriteTx({}, async (tx) => { + const coinSelRes = await selectPeerCoinsInTx(wex, tx, { + instructedAmount, + // Any (single!) exchange that is in scope works. + restrictScope: req.restrictScope, + feesCoveredByCounterparty: false, + }); - switch (coinSelRes.type) { - case "failure": - throw TalerError.fromDetail( - TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE, - { - insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails, - }, - ); - case "prospective": - coins = coinSelRes.result.prospectiveCoins; - break; - case "success": - coins = coinSelRes.result.coins; - break; - default: - assertUnreachable(coinSelRes); - } + let coins: SelectedProspectiveCoin[] | undefined = undefined; - logger.trace(j2s(coinSelRes)); + switch (coinSelRes.type) { + case "failure": + throw TalerError.fromDetail( + TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE, + { + insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails, + }, + ); + case "prospective": + coins = coinSelRes.result.prospectiveCoins; + break; + case "success": + coins = coinSelRes.result.coins; + break; + default: + assertUnreachable(coinSelRes); + } - const sel = coinSelRes.result; + logger.trace(j2s(coinSelRes)); - logger.trace( - `peer debit instructed amount: ${Amounts.stringify(instructedAmount)}`, - ); - logger.trace( - `peer debit contract terms amount: ${Amounts.stringify( - contractTerms.amount, - )}`, - ); - logger.trace( - `peer debit deposit fees: ${Amounts.stringify(sel.totalDepositFees)}`, - ); + const sel = coinSelRes.result; - const totalAmount = await getTotalPeerPaymentCostInTx(wex, tx, coins); - const ppi: PeerPushDebitRecord = { - amount: Amounts.stringify(instructedAmount), - restrictScope: req.restrictScope, - contractPriv: contractKeyPair.priv, - contractPub: contractKeyPair.pub, - contractTermsHash: hContractTerms, - exchangeBaseUrl: sel.exchangeBaseUrl, - mergePriv: mergePair.priv, - mergePub: mergePair.pub, - purseExpiration: timestampProtocolToDb(purseExpiration), - pursePriv: pursePair.priv, - pursePub: pursePair.pub, - timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()), - status: PeerPushDebitStatus.PendingCreatePurse, - contractEncNonce, - totalCost: Amounts.stringify(totalAmount), - }; + logger.trace( + `peer debit instructed amount: ${Amounts.stringify(instructedAmount)}`, + ); + logger.trace( + `peer debit contract terms amount: ${Amounts.stringify( + contractTerms.amount, + )}`, + ); + logger.trace( + `peer debit deposit fees: ${Amounts.stringify(sel.totalDepositFees)}`, + ); - if (coinSelRes.type === "success") { - ppi.coinSel = { - coinPubs: coinSelRes.result.coins.map((x) => x.coinPub), - contributions: coinSelRes.result.coins.map((x) => x.contribution), - }; - // FIXME: Instead of directly doing a spendCoin here, - // we might want to mark the coins as used and spend them - // after we've been able to create the purse. - await spendCoins(wex, tx, { - transactionId: ctx.transactionId, - coinPubs: coinSelRes.result.coins.map((x) => x.coinPub), - contributions: coinSelRes.result.coins.map((x) => - Amounts.parseOrThrow(x.contribution), - ), - refreshReason: RefreshReason.PayPeerPush, - }); - } - await tx.contractTerms.put({ - h: hContractTerms, - contractTermsRaw: contractTerms, + const totalAmount = await getTotalPeerPaymentCostInTx(wex, tx, coins); + const ppi: PeerPushDebitRecord = { + amount: Amounts.stringify(instructedAmount), + restrictScope: req.restrictScope, + contractPriv: contractKeyPair.priv, + contractPub: contractKeyPair.pub, + contractTermsHash: hContractTerms, + exchangeBaseUrl: sel.exchangeBaseUrl, + mergePriv: mergePair.priv, + mergePub: mergePair.pub, + purseExpiration: timestampProtocolToDb(purseExpiration), + pursePriv: pursePair.priv, + pursePub: pursePair.pub, + timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()), + status: PeerPushDebitStatus.PendingCreatePurse, + contractEncNonce, + totalCost: Amounts.stringify(totalAmount), + }; + + if (coinSelRes.type === "success") { + ppi.coinSel = { + coinPubs: coinSelRes.result.coins.map((x) => x.coinPub), + contributions: coinSelRes.result.coins.map((x) => x.contribution), + }; + // FIXME: Instead of directly doing a spendCoin here, + // we might want to mark the coins as used and spend them + // after we've been able to create the purse. + await spendCoins(wex, tx, { + transactionId: ctx.transactionId, + coinPubs: coinSelRes.result.coins.map((x) => x.coinPub), + contributions: coinSelRes.result.coins.map((x) => + Amounts.parseOrThrow(x.contribution), + ), + refreshReason: RefreshReason.PayPeerPush, }); - exchangeBaseUrl = coinSelRes.result.exchangeBaseUrl; - return ppi; - }, - ); + } + await tx.contractTerms.put({ + h: hContractTerms, + contractTermsRaw: contractTerms, + }); + exchangeBaseUrl = coinSelRes.result.exchangeBaseUrl; + const [oldRec, h] = await ctx.getRecordHandle(tx); + if (oldRec) { + throw Error("record for peer-push-debit already exists"); + } + await h.update(ppi); + }); wex.taskScheduler.startShepherdTask(ctx.taskId);