@bsv/sdk 1.10.4 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/mod.js +1 -0
- package/dist/cjs/mod.js.map +1 -1
- package/dist/cjs/package.json +2 -3
- package/dist/cjs/src/auth/Peer.js +18 -20
- package/dist/cjs/src/auth/Peer.js.map +1 -1
- package/dist/cjs/src/identity/IdentityClient.js +20 -124
- package/dist/cjs/src/identity/IdentityClient.js.map +1 -1
- package/dist/cjs/src/primitives/TransactionSignature.js +115 -10
- package/dist/cjs/src/primitives/TransactionSignature.js.map +1 -1
- package/dist/cjs/src/primitives/utils.js +13 -112
- package/dist/cjs/src/primitives/utils.js.map +1 -1
- package/dist/cjs/src/remittance/CommsLayer.js +3 -0
- package/dist/cjs/src/remittance/CommsLayer.js.map +1 -0
- package/dist/cjs/src/remittance/IdentityLayer.js +3 -0
- package/dist/cjs/src/remittance/IdentityLayer.js.map +1 -0
- package/dist/cjs/src/remittance/RemittanceManager.js +1245 -0
- package/dist/cjs/src/remittance/RemittanceManager.js.map +1 -0
- package/dist/cjs/src/remittance/RemittanceModule.js +3 -0
- package/dist/cjs/src/remittance/RemittanceModule.js.map +1 -0
- package/dist/cjs/src/remittance/index.js +23 -0
- package/dist/cjs/src/remittance/index.js.map +1 -0
- package/dist/cjs/src/remittance/modules/BasicBRC29.js +225 -0
- package/dist/cjs/src/remittance/modules/BasicBRC29.js.map +1 -0
- package/dist/cjs/src/remittance/modules/index.js +18 -0
- package/dist/cjs/src/remittance/modules/index.js.map +1 -0
- package/dist/cjs/src/remittance/types.js +22 -0
- package/dist/cjs/src/remittance/types.js.map +1 -0
- package/dist/cjs/src/script/OP.js +15 -13
- package/dist/cjs/src/script/OP.js.map +1 -1
- package/dist/cjs/src/script/Script.js +4 -1
- package/dist/cjs/src/script/Script.js.map +1 -1
- package/dist/cjs/src/script/Spend.js +128 -46
- package/dist/cjs/src/script/Spend.js.map +1 -1
- package/dist/cjs/src/transaction/BeefTx.js +2 -2
- package/dist/cjs/src/transaction/Transaction.js +160 -0
- package/dist/cjs/src/transaction/Transaction.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/mod.js +1 -0
- package/dist/esm/mod.js.map +1 -1
- package/dist/esm/src/auth/Peer.js +18 -20
- package/dist/esm/src/auth/Peer.js.map +1 -1
- package/dist/esm/src/identity/IdentityClient.js +20 -124
- package/dist/esm/src/identity/IdentityClient.js.map +1 -1
- package/dist/esm/src/primitives/TransactionSignature.js +115 -10
- package/dist/esm/src/primitives/TransactionSignature.js.map +1 -1
- package/dist/esm/src/primitives/utils.js +13 -112
- package/dist/esm/src/primitives/utils.js.map +1 -1
- package/dist/esm/src/remittance/CommsLayer.js +2 -0
- package/dist/esm/src/remittance/CommsLayer.js.map +1 -0
- package/dist/esm/src/remittance/IdentityLayer.js +2 -0
- package/dist/esm/src/remittance/IdentityLayer.js.map +1 -0
- package/dist/esm/src/remittance/RemittanceManager.js +1254 -0
- package/dist/esm/src/remittance/RemittanceManager.js.map +1 -0
- package/dist/esm/src/remittance/RemittanceModule.js +2 -0
- package/dist/esm/src/remittance/RemittanceModule.js.map +1 -0
- package/dist/esm/src/remittance/index.js +7 -0
- package/dist/esm/src/remittance/index.js.map +1 -0
- package/dist/esm/src/remittance/modules/BasicBRC29.js +227 -0
- package/dist/esm/src/remittance/modules/BasicBRC29.js.map +1 -0
- package/dist/esm/src/remittance/modules/index.js +2 -0
- package/dist/esm/src/remittance/modules/index.js.map +1 -0
- package/dist/esm/src/remittance/types.js +19 -0
- package/dist/esm/src/remittance/types.js.map +1 -0
- package/dist/esm/src/script/OP.js +15 -13
- package/dist/esm/src/script/OP.js.map +1 -1
- package/dist/esm/src/script/Script.js +4 -1
- package/dist/esm/src/script/Script.js.map +1 -1
- package/dist/esm/src/script/Spend.js +129 -46
- package/dist/esm/src/script/Spend.js.map +1 -1
- package/dist/esm/src/transaction/BeefTx.js +3 -3
- package/dist/esm/src/transaction/BeefTx.js.map +1 -1
- package/dist/esm/src/transaction/Transaction.js +160 -0
- package/dist/esm/src/transaction/Transaction.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/mod.d.ts +1 -0
- package/dist/types/mod.d.ts.map +1 -1
- package/dist/types/src/auth/Peer.d.ts +3 -7
- package/dist/types/src/auth/Peer.d.ts.map +1 -1
- package/dist/types/src/identity/IdentityClient.d.ts +0 -8
- package/dist/types/src/identity/IdentityClient.d.ts.map +1 -1
- package/dist/types/src/primitives/TransactionSignature.d.ts +16 -4
- package/dist/types/src/primitives/TransactionSignature.d.ts.map +1 -1
- package/dist/types/src/primitives/utils.d.ts +1 -0
- package/dist/types/src/primitives/utils.d.ts.map +1 -1
- package/dist/types/src/remittance/CommsLayer.d.ts +50 -0
- package/dist/types/src/remittance/CommsLayer.d.ts.map +1 -0
- package/dist/types/src/remittance/IdentityLayer.d.ts +35 -0
- package/dist/types/src/remittance/IdentityLayer.d.ts.map +1 -0
- package/dist/types/src/remittance/RemittanceManager.d.ts +452 -0
- package/dist/types/src/remittance/RemittanceManager.d.ts.map +1 -0
- package/dist/types/src/remittance/RemittanceModule.d.ts +106 -0
- package/dist/types/src/remittance/RemittanceModule.d.ts.map +1 -0
- package/dist/types/src/remittance/index.d.ts +7 -0
- package/dist/types/src/remittance/index.d.ts.map +1 -0
- package/dist/types/src/remittance/modules/BasicBRC29.d.ts +133 -0
- package/dist/types/src/remittance/modules/BasicBRC29.d.ts.map +1 -0
- package/dist/types/src/remittance/modules/index.d.ts +2 -0
- package/dist/types/src/remittance/modules/index.d.ts.map +1 -0
- package/dist/types/src/remittance/types.d.ts +238 -0
- package/dist/types/src/remittance/types.d.ts.map +1 -0
- package/dist/types/src/script/OP.d.ts +5 -3
- package/dist/types/src/script/OP.d.ts.map +1 -1
- package/dist/types/src/script/Script.d.ts.map +1 -1
- package/dist/types/src/script/Spend.d.ts +7 -0
- package/dist/types/src/script/Spend.d.ts.map +1 -1
- package/dist/types/src/transaction/BeefTx.d.ts +2 -2
- package/dist/types/src/transaction/Transaction.d.ts +14 -0
- package/dist/types/src/transaction/Transaction.d.ts.map +1 -1
- package/dist/types/src/wallet/Wallet.interfaces.d.ts +5 -5
- package/dist/types/src/wallet/Wallet.interfaces.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/dist/umd/bundle.js +13 -13
- package/dist/umd/bundle.js.map +1 -1
- package/docs/index.md +2 -14
- package/docs/reference/auth.md +6 -12
- package/docs/reference/primitives.md +20 -78
- package/docs/reference/remittance.md +2166 -0
- package/docs/reference/script.md +11 -3
- package/docs/reference/transaction.md +27 -1
- package/docs/reference/wallet.md +6 -5
- package/docs/remittance-getting-started.md +138 -0
- package/mod.ts +1 -0
- package/package.json +12 -3
- package/src/auth/Peer.ts +18 -29
- package/src/auth/__tests/Peer.test.ts +253 -1
- package/src/identity/IdentityClient.ts +29 -153
- package/src/identity/__tests/IdentityClient.test.ts +1 -289
- package/src/overlay-tools/__tests/SHIPBroadcaster.test.ts +7 -9
- package/src/primitives/TransactionSignature.ts +129 -10
- package/src/primitives/__tests/utils.test.ts +30 -7
- package/src/primitives/utils.ts +13 -129
- package/src/remittance/CommsLayer.ts +41 -0
- package/src/remittance/IdentityLayer.ts +32 -0
- package/src/remittance/RemittanceManager.ts +1672 -0
- package/src/remittance/RemittanceModule.ts +92 -0
- package/src/remittance/__tests/BasicBRC29.test.ts +188 -0
- package/src/remittance/__tests/RemittanceManager.test.ts +493 -0
- package/src/remittance/__tests/examples.ts +130 -0
- package/src/remittance/index.ts +6 -0
- package/src/remittance/modules/BasicBRC29.ts +361 -0
- package/src/remittance/modules/index.ts +1 -0
- package/src/remittance/types.ts +284 -0
- package/src/script/OP.ts +15 -13
- package/src/script/Script.ts +3 -1
- package/src/script/Spend.ts +128 -52
- package/src/script/__tests/Chronicle.test.ts +186 -0
- package/src/script/__tests/Spend.test.ts +1 -1
- package/src/script/__tests/SpendValildVectors.test.ts +63 -0
- package/src/script/__tests/lrshiftnum.test.ts +185 -0
- package/src/script/__tests/sighashTestData.ts +1031 -0
- package/src/script/__tests/spend.valid.vectors.ts +9 -16
- package/src/transaction/BeefTx.ts +3 -3
- package/src/transaction/Transaction.ts +186 -0
- package/src/transaction/__tests/Beef.test.ts +2 -0
- package/src/transaction/__tests/Transaction.test.ts +641 -3
- package/src/wallet/Wallet.interfaces.ts +5 -5
|
@@ -0,0 +1,1245 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.InvoiceHandle = exports.ThreadHandle = exports.RemittanceManager = exports.DEFAULT_REMITTANCE_MESSAGEBOX = void 0;
|
|
7
|
+
const types_js_1 = require("./types.js");
|
|
8
|
+
const utils_js_1 = require("../primitives/utils.js");
|
|
9
|
+
const Random_js_1 = __importDefault(require("../primitives/Random.js"));
|
|
10
|
+
exports.DEFAULT_REMITTANCE_MESSAGEBOX = 'remittance_inbox';
|
|
11
|
+
/**
|
|
12
|
+
* RemittanceManager.
|
|
13
|
+
*
|
|
14
|
+
* Responsibilities:
|
|
15
|
+
* - message transport via CommsLayer
|
|
16
|
+
* - thread lifecycle and persistence (via stateSaver/stateLoader)
|
|
17
|
+
* - invoice creation and transmission (when invoices are used)
|
|
18
|
+
* - settlement and settlement routing to the appropriate module
|
|
19
|
+
* - receipt issuance and receipt routing to the appropriate module
|
|
20
|
+
* - identity and identity certificate exchange (when identity layer is used)
|
|
21
|
+
*
|
|
22
|
+
* Non-responsibilities (left to modules):
|
|
23
|
+
* - transaction structure (whether UTXO “offer” formats, token logic, BRC-98/99 specifics, etc.)
|
|
24
|
+
* - validation rules for settlement (e.g. partial tx templates, UTXO validity, etc.)
|
|
25
|
+
* - on-chain broadcasting strategy or non-chain settlement specifics (like legacy payment protocols)
|
|
26
|
+
* - Providing option terms for invoices
|
|
27
|
+
* - Building settlement artifacts
|
|
28
|
+
* - Accepting/rejecting settlements
|
|
29
|
+
* - Deciding which identity certificates to request
|
|
30
|
+
* - Deciding about sufficiency of identity certificates
|
|
31
|
+
* - Preparing/processing specific receipt formats
|
|
32
|
+
* - Internal business logic like order fulfillment, refunds, etc.
|
|
33
|
+
*/
|
|
34
|
+
class RemittanceManager {
|
|
35
|
+
constructor(cfg, wallet, commsLayer, threads = []) {
|
|
36
|
+
this.cfg = cfg;
|
|
37
|
+
this.wallet = wallet;
|
|
38
|
+
this.comms = commsLayer;
|
|
39
|
+
this.messageBox = cfg.messageBox ?? exports.DEFAULT_REMITTANCE_MESSAGEBOX;
|
|
40
|
+
this.now = cfg.now ?? (() => Date.now());
|
|
41
|
+
this.threadIdFactory = cfg.threadIdFactory ?? defaultThreadIdFactory;
|
|
42
|
+
this.moduleRegistry = new Map(cfg.remittanceModules.map((m) => [m.id, m]));
|
|
43
|
+
this.eventListeners = new Set();
|
|
44
|
+
this.stateWaiters = new Map();
|
|
45
|
+
this.eventHandlers = cfg.events;
|
|
46
|
+
if (typeof cfg.onEvent === 'function') {
|
|
47
|
+
this.eventListeners.add(cfg.onEvent);
|
|
48
|
+
}
|
|
49
|
+
this.runtime = {
|
|
50
|
+
identityOptions: cfg.options?.identityOptions ?? {
|
|
51
|
+
makerRequestIdentity: 'never',
|
|
52
|
+
takerRequestIdentity: 'never'
|
|
53
|
+
},
|
|
54
|
+
receiptProvided: cfg.options?.receiptProvided ?? true,
|
|
55
|
+
autoIssueReceipt: cfg.options?.autoIssueReceipt ?? true,
|
|
56
|
+
invoiceExpirySeconds: cfg.options?.invoiceExpirySeconds ?? 3600,
|
|
57
|
+
identityTimeoutMs: cfg.options?.identityTimeoutMs ?? 30000,
|
|
58
|
+
identityPollIntervalMs: cfg.options?.identityPollIntervalMs ?? 500
|
|
59
|
+
};
|
|
60
|
+
this.threads = threads.map((thread) => this.ensureThreadState(thread));
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Loads persisted state from cfg.stateLoader (if provided).
|
|
64
|
+
*
|
|
65
|
+
* Safe to call multiple times.
|
|
66
|
+
*/
|
|
67
|
+
async init() {
|
|
68
|
+
if (typeof this.cfg.stateLoader !== 'function')
|
|
69
|
+
return;
|
|
70
|
+
const loaded = await this.cfg.stateLoader();
|
|
71
|
+
if (typeof loaded !== 'object')
|
|
72
|
+
return;
|
|
73
|
+
this.loadState(loaded);
|
|
74
|
+
if (typeof loaded.defaultPaymentOptionId === 'string') {
|
|
75
|
+
this.defaultPaymentOptionId = loaded.defaultPaymentOptionId;
|
|
76
|
+
}
|
|
77
|
+
await this.refreshMyIdentityKey();
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Registers a remittance event listener.
|
|
81
|
+
*/
|
|
82
|
+
onEvent(listener) {
|
|
83
|
+
this.eventListeners.add(listener);
|
|
84
|
+
return () => {
|
|
85
|
+
this.eventListeners.delete(listener);
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Sets a default payment option (module id) to use when paying invoices.
|
|
90
|
+
*/
|
|
91
|
+
preselectPaymentOption(optionId) {
|
|
92
|
+
this.defaultPaymentOptionId = optionId;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Returns an immutable snapshot of current manager state suitable for persistence.
|
|
96
|
+
*/
|
|
97
|
+
saveState() {
|
|
98
|
+
return {
|
|
99
|
+
v: 1,
|
|
100
|
+
threads: JSON.parse(JSON.stringify(this.threads)),
|
|
101
|
+
defaultPaymentOptionId: this.defaultPaymentOptionId
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Loads state from an object previously produced by saveState().
|
|
106
|
+
*/
|
|
107
|
+
loadState(state) {
|
|
108
|
+
if (state.v !== 1)
|
|
109
|
+
throw new Error('Unsupported RemittanceManagerState version');
|
|
110
|
+
this.threads = (state.threads ?? []).map((thread) => this.ensureThreadState(thread));
|
|
111
|
+
this.defaultPaymentOptionId = state.defaultPaymentOptionId;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Persists current state via cfg.stateSaver (if provided).
|
|
115
|
+
*/
|
|
116
|
+
async persistState() {
|
|
117
|
+
if (this.cfg.stateSaver == null)
|
|
118
|
+
return;
|
|
119
|
+
await this.cfg.stateSaver(this.saveState());
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Syncs threads by fetching pending messages from the comms layer and processing them.
|
|
123
|
+
*
|
|
124
|
+
* Processing is idempotent using transport messageIds tracked per thread.
|
|
125
|
+
* Messages are acknowledged after they are successfully applied to local state.
|
|
126
|
+
*/
|
|
127
|
+
async syncThreads(hostOverride) {
|
|
128
|
+
await this.refreshMyIdentityKey();
|
|
129
|
+
const msgs = await this.comms.listMessages({ messageBox: this.messageBox, host: hostOverride });
|
|
130
|
+
for (const msg of msgs) {
|
|
131
|
+
await this.handleInboundMessage(msg);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Starts listening for live messages (if the CommsLayer supports it).
|
|
136
|
+
*/
|
|
137
|
+
async startListening(hostOverride) {
|
|
138
|
+
if (typeof this.comms.listenForLiveMessages !== 'function') {
|
|
139
|
+
throw new Error('CommsLayer does not support live message listening');
|
|
140
|
+
}
|
|
141
|
+
await this.comms.listenForLiveMessages({
|
|
142
|
+
messageBox: this.messageBox,
|
|
143
|
+
overrideHost: hostOverride,
|
|
144
|
+
onMessage: (msg) => {
|
|
145
|
+
void this.handleInboundMessage(msg);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Creates, records, and sends an invoice to a counterparty.
|
|
151
|
+
*
|
|
152
|
+
* Returns a handle you can use to wait for payment/receipt.
|
|
153
|
+
*/
|
|
154
|
+
async sendInvoice(to, input, hostOverride) {
|
|
155
|
+
await this.refreshMyIdentityKey();
|
|
156
|
+
const threadId = this.threadIdFactory();
|
|
157
|
+
const createdAt = this.now();
|
|
158
|
+
const myKey = this.requireMyIdentityKey('sendInvoice requires the wallet to provide an identity key');
|
|
159
|
+
const thread = {
|
|
160
|
+
threadId,
|
|
161
|
+
counterparty: to,
|
|
162
|
+
myRole: 'maker',
|
|
163
|
+
theirRole: 'taker',
|
|
164
|
+
createdAt,
|
|
165
|
+
updatedAt: createdAt,
|
|
166
|
+
state: 'new',
|
|
167
|
+
stateLog: [],
|
|
168
|
+
processedMessageIds: [],
|
|
169
|
+
protocolLog: [],
|
|
170
|
+
identity: {
|
|
171
|
+
certsSent: [],
|
|
172
|
+
certsReceived: [],
|
|
173
|
+
requestSent: false,
|
|
174
|
+
responseSent: false,
|
|
175
|
+
acknowledgmentSent: false,
|
|
176
|
+
acknowledgmentReceived: false
|
|
177
|
+
},
|
|
178
|
+
flags: {
|
|
179
|
+
hasIdentified: false,
|
|
180
|
+
hasInvoiced: false,
|
|
181
|
+
hasPaid: false,
|
|
182
|
+
hasReceipted: false,
|
|
183
|
+
error: false
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
this.threads.push(thread);
|
|
187
|
+
this.emitEvent({ type: 'threadCreated', threadId: thread.threadId, thread });
|
|
188
|
+
if (thread.identity.responseSent && !thread.flags.hasIdentified) {
|
|
189
|
+
await this.waitForIdentityAcknowledgment(threadId, {
|
|
190
|
+
timeoutMs: this.runtime.identityTimeoutMs,
|
|
191
|
+
pollIntervalMs: this.runtime.identityPollIntervalMs
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
if (this.shouldRequestIdentity(thread, 'beforeInvoicing')) {
|
|
195
|
+
await this.ensureIdentityExchange(thread, hostOverride);
|
|
196
|
+
}
|
|
197
|
+
const invoice = await this.composeInvoice(threadId, myKey, to, input);
|
|
198
|
+
thread.invoice = invoice;
|
|
199
|
+
thread.flags.hasInvoiced = true;
|
|
200
|
+
this.transitionThreadState(thread, 'invoiced', 'invoice created');
|
|
201
|
+
// Generate option terms for each configured module.
|
|
202
|
+
for (const mod of this.moduleRegistry.values()) {
|
|
203
|
+
if (typeof mod.createOption !== 'function')
|
|
204
|
+
continue;
|
|
205
|
+
const option = await mod.createOption({ threadId, invoice }, this.moduleContext());
|
|
206
|
+
invoice.options[mod.id] = option;
|
|
207
|
+
}
|
|
208
|
+
const env = this.makeEnvelope('invoice', threadId, invoice);
|
|
209
|
+
const mid = await this.sendEnvelope(to, env, hostOverride);
|
|
210
|
+
thread.protocolLog.push({ direction: 'out', envelope: env, transportMessageId: mid });
|
|
211
|
+
this.emitEvent({ type: 'invoiceSent', threadId: thread.threadId, invoice });
|
|
212
|
+
thread.updatedAt = this.now();
|
|
213
|
+
await this.persistState();
|
|
214
|
+
return new InvoiceHandle(this, threadId);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Sends an invoice for an existing thread, e.g. after an identity request was received.
|
|
218
|
+
*/
|
|
219
|
+
async sendInvoiceForThread(threadId, input, hostOverride) {
|
|
220
|
+
await this.refreshMyIdentityKey();
|
|
221
|
+
const thread = this.getThreadOrThrow(threadId);
|
|
222
|
+
if (thread.flags.error)
|
|
223
|
+
throw new Error('Thread is in error state');
|
|
224
|
+
if (thread.myRole !== 'maker')
|
|
225
|
+
throw new Error('Only makers can send invoices');
|
|
226
|
+
if (thread.invoice != null)
|
|
227
|
+
throw new Error('Thread already has an invoice');
|
|
228
|
+
if (thread.identity.responseSent && !thread.flags.hasIdentified) {
|
|
229
|
+
await this.waitForIdentityAcknowledgment(threadId, {
|
|
230
|
+
timeoutMs: this.runtime.identityTimeoutMs,
|
|
231
|
+
pollIntervalMs: this.runtime.identityPollIntervalMs
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
if (this.shouldRequestIdentity(thread, 'beforeInvoicing')) {
|
|
235
|
+
await this.ensureIdentityExchange(thread, hostOverride);
|
|
236
|
+
}
|
|
237
|
+
const myKey = this.requireMyIdentityKey('sendInvoice requires the wallet to provide an identity key');
|
|
238
|
+
const invoice = await this.composeInvoice(threadId, myKey, thread.counterparty, input);
|
|
239
|
+
thread.invoice = invoice;
|
|
240
|
+
thread.flags.hasInvoiced = true;
|
|
241
|
+
this.transitionThreadState(thread, 'invoiced', 'invoice created');
|
|
242
|
+
for (const mod of this.moduleRegistry.values()) {
|
|
243
|
+
if (typeof mod.createOption !== 'function')
|
|
244
|
+
continue;
|
|
245
|
+
const option = await mod.createOption({ threadId, invoice }, this.moduleContext());
|
|
246
|
+
invoice.options[mod.id] = option;
|
|
247
|
+
}
|
|
248
|
+
const env = this.makeEnvelope('invoice', threadId, invoice);
|
|
249
|
+
const mid = await this.sendEnvelope(thread.counterparty, env, hostOverride);
|
|
250
|
+
thread.protocolLog.push({ direction: 'out', envelope: env, transportMessageId: mid });
|
|
251
|
+
this.emitEvent({ type: 'invoiceSent', threadId: thread.threadId, invoice });
|
|
252
|
+
thread.updatedAt = this.now();
|
|
253
|
+
await this.persistState();
|
|
254
|
+
return new InvoiceHandle(this, threadId);
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Returns invoice handles that this manager can pay (we are the taker/payer).
|
|
258
|
+
*/
|
|
259
|
+
findInvoicesPayable(counterparty) {
|
|
260
|
+
const hasCounterparty = typeof counterparty === 'string' && counterparty.length > 0;
|
|
261
|
+
return this.threads
|
|
262
|
+
.filter((t) => t.myRole === 'taker' && (t.invoice != null) && (t.settlement == null) && !t.flags.error)
|
|
263
|
+
.filter((t) => (hasCounterparty ? t.counterparty === counterparty : true))
|
|
264
|
+
.map((t) => new InvoiceHandle(this, t.threadId));
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Returns invoice handles that we issued and are waiting to receive settlement for.
|
|
268
|
+
*/
|
|
269
|
+
findReceivableInvoices(counterparty) {
|
|
270
|
+
const hasCounterparty = typeof counterparty === 'string' && counterparty.length > 0;
|
|
271
|
+
return this.threads
|
|
272
|
+
.filter((t) => t.myRole === 'maker' && (t.invoice != null) && (t.settlement == null) && !t.flags.error)
|
|
273
|
+
.filter((t) => (hasCounterparty ? t.counterparty === counterparty : true))
|
|
274
|
+
.map((t) => new InvoiceHandle(this, t.threadId));
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Pays an invoice by selecting a remittance option and sending a settlement message.
|
|
278
|
+
*
|
|
279
|
+
* If receipts are enabled (receiptProvided), this method will optionally wait for a receipt.
|
|
280
|
+
*/
|
|
281
|
+
async pay(threadId, optionId, hostOverride) {
|
|
282
|
+
await this.refreshMyIdentityKey();
|
|
283
|
+
const thread = this.getThreadOrThrow(threadId);
|
|
284
|
+
if (thread.invoice == null)
|
|
285
|
+
throw new Error('Thread has no invoice to pay');
|
|
286
|
+
if (thread.flags.error)
|
|
287
|
+
throw new Error('Thread is in error state');
|
|
288
|
+
if (thread.settlement != null)
|
|
289
|
+
throw new Error('Invoice already paid (settlement exists)');
|
|
290
|
+
if (thread.identity.responseSent && !thread.flags.hasIdentified) {
|
|
291
|
+
await this.waitForIdentityAcknowledgment(threadId, {
|
|
292
|
+
timeoutMs: this.runtime.identityTimeoutMs,
|
|
293
|
+
pollIntervalMs: this.runtime.identityPollIntervalMs
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
if (this.shouldRequestIdentity(thread, 'beforeSettlement')) {
|
|
297
|
+
await this.ensureIdentityExchange(thread, hostOverride);
|
|
298
|
+
}
|
|
299
|
+
// Check expiry.
|
|
300
|
+
const expiresAt = thread.invoice.expiresAt;
|
|
301
|
+
if (typeof expiresAt === 'number' && this.now() > expiresAt) {
|
|
302
|
+
throw new Error('Invoice is expired');
|
|
303
|
+
}
|
|
304
|
+
const chosenOptionId = optionId ?? this.defaultPaymentOptionId ?? Object.keys(thread.invoice.options)[0];
|
|
305
|
+
if (chosenOptionId == null || chosenOptionId === '') {
|
|
306
|
+
throw new Error('No remittance options available on invoice');
|
|
307
|
+
}
|
|
308
|
+
const module = this.moduleRegistry.get(chosenOptionId);
|
|
309
|
+
if (module == null) {
|
|
310
|
+
throw new Error(`No configured remittance module for option: ${chosenOptionId}`);
|
|
311
|
+
}
|
|
312
|
+
const option = thread.invoice.options[chosenOptionId];
|
|
313
|
+
const myKey = this.requireMyIdentityKey('pay() requires the wallet to provide an identity key');
|
|
314
|
+
const buildResult = await module.buildSettlement({ threadId, invoice: thread.invoice, option, note: thread.invoice.note }, this.moduleContext());
|
|
315
|
+
if (buildResult.action === 'terminate') {
|
|
316
|
+
const termination = buildResult.termination;
|
|
317
|
+
await this.sendTermination(thread, thread.counterparty, termination.message, termination.details, termination.code);
|
|
318
|
+
await this.persistState();
|
|
319
|
+
return termination;
|
|
320
|
+
}
|
|
321
|
+
const settlement = {
|
|
322
|
+
kind: 'settlement',
|
|
323
|
+
threadId,
|
|
324
|
+
moduleId: module.id,
|
|
325
|
+
optionId: chosenOptionId,
|
|
326
|
+
sender: myKey,
|
|
327
|
+
createdAt: this.now(),
|
|
328
|
+
artifact: buildResult.artifact,
|
|
329
|
+
note: thread.invoice.note
|
|
330
|
+
};
|
|
331
|
+
const env = this.makeEnvelope('settlement', threadId, settlement);
|
|
332
|
+
// Send settlement to payee (invoice.payee).
|
|
333
|
+
const mid = await this.sendEnvelope(thread.invoice.payee, env, hostOverride);
|
|
334
|
+
thread.protocolLog.push({ direction: 'out', envelope: env, transportMessageId: mid });
|
|
335
|
+
this.emitEvent({ type: 'settlementSent', threadId: thread.threadId, settlement });
|
|
336
|
+
thread.settlement = settlement;
|
|
337
|
+
thread.flags.hasPaid = true;
|
|
338
|
+
this.transitionThreadState(thread, 'settled', 'settlement sent');
|
|
339
|
+
thread.updatedAt = this.now();
|
|
340
|
+
await this.persistState();
|
|
341
|
+
if (!this.runtime.receiptProvided) {
|
|
342
|
+
return undefined;
|
|
343
|
+
}
|
|
344
|
+
// Wait for receipt (polling + syncThreads) up to a default timeout.
|
|
345
|
+
return await this.waitForReceipt(threadId);
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Waits for a receipt to arrive for a thread.
|
|
349
|
+
*
|
|
350
|
+
* Uses polling via syncThreads because live listeners are optional.
|
|
351
|
+
*/
|
|
352
|
+
async waitForReceipt(threadId, opts = {}) {
|
|
353
|
+
const timeoutMs = opts.timeoutMs ?? 30000;
|
|
354
|
+
const pollIntervalMs = opts.pollIntervalMs ?? 500;
|
|
355
|
+
const start = this.now();
|
|
356
|
+
while (this.now() - start < timeoutMs) {
|
|
357
|
+
const t = this.getThreadOrThrow(threadId);
|
|
358
|
+
if (typeof t.receipt === 'object')
|
|
359
|
+
return t.receipt;
|
|
360
|
+
if (typeof t.termination === 'object')
|
|
361
|
+
return t.termination;
|
|
362
|
+
await this.syncThreads();
|
|
363
|
+
await sleep(pollIntervalMs);
|
|
364
|
+
}
|
|
365
|
+
throw new Error('Timed out waiting for receipt');
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Waits for a thread to reach a specific state.
|
|
369
|
+
*/
|
|
370
|
+
async waitForState(threadId, state, opts = {}) {
|
|
371
|
+
const timeoutMs = opts.timeoutMs ?? 30000;
|
|
372
|
+
const pollIntervalMs = opts.pollIntervalMs ?? 500;
|
|
373
|
+
const start = this.now();
|
|
374
|
+
const t = this.getThreadOrThrow(threadId);
|
|
375
|
+
if (t.state === state)
|
|
376
|
+
return t;
|
|
377
|
+
if (t.state === 'terminated' || t.state === 'errored') {
|
|
378
|
+
throw new Error(`Thread entered terminal state: ${t.state}`);
|
|
379
|
+
}
|
|
380
|
+
let settled = false;
|
|
381
|
+
let timedOut = false;
|
|
382
|
+
let resolvePromise;
|
|
383
|
+
let rejectPromise;
|
|
384
|
+
const entry = {
|
|
385
|
+
state,
|
|
386
|
+
resolve: () => {
|
|
387
|
+
if (settled || timedOut)
|
|
388
|
+
return;
|
|
389
|
+
settled = true;
|
|
390
|
+
resolvePromise();
|
|
391
|
+
},
|
|
392
|
+
reject: (err) => {
|
|
393
|
+
if (settled || timedOut)
|
|
394
|
+
return;
|
|
395
|
+
settled = true;
|
|
396
|
+
rejectPromise(err);
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
const waiter = new Promise((resolve, reject) => {
|
|
400
|
+
resolvePromise = resolve;
|
|
401
|
+
rejectPromise = reject;
|
|
402
|
+
const waiters = this.stateWaiters.get(threadId) ?? [];
|
|
403
|
+
waiters.push(entry);
|
|
404
|
+
this.stateWaiters.set(threadId, waiters);
|
|
405
|
+
});
|
|
406
|
+
const removeEntry = () => {
|
|
407
|
+
const waiters = this.stateWaiters.get(threadId);
|
|
408
|
+
if (waiters == null)
|
|
409
|
+
return;
|
|
410
|
+
const remaining = waiters.filter((item) => item !== entry);
|
|
411
|
+
if (remaining.length === 0) {
|
|
412
|
+
this.stateWaiters.delete(threadId);
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
this.stateWaiters.set(threadId, remaining);
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
const poller = (async () => {
|
|
419
|
+
while (this.now() - start < timeoutMs) {
|
|
420
|
+
if (settled)
|
|
421
|
+
return;
|
|
422
|
+
const current = this.getThreadOrThrow(threadId);
|
|
423
|
+
if (current.state === state) {
|
|
424
|
+
this.resolveStateWaiters(threadId, state);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (current.state === 'terminated' || current.state === 'errored') {
|
|
428
|
+
throw new Error(`Thread entered terminal state: ${current.state}`);
|
|
429
|
+
}
|
|
430
|
+
await this.syncThreads();
|
|
431
|
+
await sleep(pollIntervalMs);
|
|
432
|
+
}
|
|
433
|
+
})();
|
|
434
|
+
await Promise.race([waiter, poller]).catch((err) => {
|
|
435
|
+
removeEntry();
|
|
436
|
+
throw err;
|
|
437
|
+
});
|
|
438
|
+
if (this.now() - start >= timeoutMs && !settled) {
|
|
439
|
+
timedOut = true;
|
|
440
|
+
removeEntry();
|
|
441
|
+
throw new Error(`Timed out waiting for state: ${state}`);
|
|
442
|
+
}
|
|
443
|
+
removeEntry();
|
|
444
|
+
return this.getThreadOrThrow(threadId);
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Waits for identity exchange to complete for a thread.
|
|
448
|
+
*/
|
|
449
|
+
async waitForIdentity(threadId, opts) {
|
|
450
|
+
return await this.waitForState(threadId, 'identityAcknowledged', opts);
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Waits for a settlement to arrive for a thread.
|
|
454
|
+
*/
|
|
455
|
+
async waitForSettlement(threadId, opts = {}) {
|
|
456
|
+
const timeoutMs = opts.timeoutMs ?? 30000;
|
|
457
|
+
const pollIntervalMs = opts.pollIntervalMs ?? 500;
|
|
458
|
+
const start = this.now();
|
|
459
|
+
while (this.now() - start < timeoutMs) {
|
|
460
|
+
const t = this.getThreadOrThrow(threadId);
|
|
461
|
+
if (typeof t.settlement === 'object')
|
|
462
|
+
return t.settlement;
|
|
463
|
+
if (typeof t.termination === 'object')
|
|
464
|
+
return t.termination;
|
|
465
|
+
await this.syncThreads();
|
|
466
|
+
await sleep(pollIntervalMs);
|
|
467
|
+
}
|
|
468
|
+
throw new Error('Timed out waiting for settlement');
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Sends an unsolicited settlement to a counterparty.
|
|
472
|
+
*/
|
|
473
|
+
async sendUnsolicitedSettlement(to, args, hostOverride) {
|
|
474
|
+
await this.refreshMyIdentityKey();
|
|
475
|
+
const module = this.moduleRegistry.get(args.moduleId);
|
|
476
|
+
if (module == null)
|
|
477
|
+
throw new Error(`No configured remittance module for option: ${args.moduleId}`);
|
|
478
|
+
if (!module.allowUnsolicitedSettlements) {
|
|
479
|
+
throw new Error(`Remittance module ${args.moduleId} does not allow unsolicited settlements`);
|
|
480
|
+
}
|
|
481
|
+
const threadId = this.threadIdFactory();
|
|
482
|
+
const createdAt = this.now();
|
|
483
|
+
const myKey = this.requireMyIdentityKey('sendUnsolicitedSettlement requires the wallet to provide an identity key');
|
|
484
|
+
const thread = {
|
|
485
|
+
threadId,
|
|
486
|
+
counterparty: to,
|
|
487
|
+
myRole: 'taker',
|
|
488
|
+
theirRole: 'maker',
|
|
489
|
+
createdAt,
|
|
490
|
+
updatedAt: createdAt,
|
|
491
|
+
state: 'new',
|
|
492
|
+
stateLog: [],
|
|
493
|
+
processedMessageIds: [],
|
|
494
|
+
protocolLog: [],
|
|
495
|
+
identity: {
|
|
496
|
+
certsSent: [],
|
|
497
|
+
certsReceived: [],
|
|
498
|
+
requestSent: false,
|
|
499
|
+
responseSent: false,
|
|
500
|
+
acknowledgmentSent: false,
|
|
501
|
+
acknowledgmentReceived: false
|
|
502
|
+
},
|
|
503
|
+
flags: {
|
|
504
|
+
hasIdentified: false,
|
|
505
|
+
hasInvoiced: false,
|
|
506
|
+
hasPaid: false,
|
|
507
|
+
hasReceipted: false,
|
|
508
|
+
error: false
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
this.threads.push(thread);
|
|
512
|
+
this.emitEvent({ type: 'threadCreated', threadId: thread.threadId, thread });
|
|
513
|
+
if (this.shouldRequestIdentity(thread, 'beforeSettlement')) {
|
|
514
|
+
await this.ensureIdentityExchange(thread, hostOverride);
|
|
515
|
+
}
|
|
516
|
+
const buildResult = await module.buildSettlement({ threadId, option: args.option, note: args.note }, this.moduleContext());
|
|
517
|
+
if (buildResult.action === 'terminate') {
|
|
518
|
+
await this.sendTermination(thread, to, buildResult.termination.message, buildResult.termination.details, buildResult.termination.code);
|
|
519
|
+
await this.persistState();
|
|
520
|
+
return new ThreadHandle(this, threadId);
|
|
521
|
+
}
|
|
522
|
+
const settlement = {
|
|
523
|
+
kind: 'settlement',
|
|
524
|
+
threadId,
|
|
525
|
+
moduleId: module.id,
|
|
526
|
+
optionId: args.optionId ?? module.id,
|
|
527
|
+
sender: myKey,
|
|
528
|
+
createdAt: this.now(),
|
|
529
|
+
artifact: buildResult.artifact,
|
|
530
|
+
note: args.note
|
|
531
|
+
};
|
|
532
|
+
const env = this.makeEnvelope('settlement', threadId, settlement);
|
|
533
|
+
const mid = await this.sendEnvelope(to, env, hostOverride);
|
|
534
|
+
thread.protocolLog.push({ direction: 'out', envelope: env, transportMessageId: mid });
|
|
535
|
+
this.emitEvent({ type: 'settlementSent', threadId: thread.threadId, settlement });
|
|
536
|
+
thread.settlement = settlement;
|
|
537
|
+
thread.flags.hasPaid = true;
|
|
538
|
+
this.transitionThreadState(thread, 'settled', 'settlement sent');
|
|
539
|
+
thread.updatedAt = this.now();
|
|
540
|
+
await this.persistState();
|
|
541
|
+
return new ThreadHandle(this, threadId);
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Returns a thread by id (if present).
|
|
545
|
+
*/
|
|
546
|
+
getThread(threadId) {
|
|
547
|
+
return this.threads.find((t) => t.threadId === threadId);
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Returns a thread handle by id, or throws if the thread does not exist.
|
|
551
|
+
*/
|
|
552
|
+
getThreadHandle(threadId) {
|
|
553
|
+
this.getThreadOrThrow(threadId);
|
|
554
|
+
return new ThreadHandle(this, threadId);
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Returns a thread by id or throws.
|
|
558
|
+
*
|
|
559
|
+
* Public so helper handles (e.g. InvoiceHandle) can call it.
|
|
560
|
+
*/
|
|
561
|
+
getThreadOrThrow(threadId) {
|
|
562
|
+
const t = this.getThread(threadId);
|
|
563
|
+
if (typeof t !== 'object')
|
|
564
|
+
throw new Error(`Unknown thread: ${threadId}`);
|
|
565
|
+
return this.ensureThreadState(t);
|
|
566
|
+
}
|
|
567
|
+
// ----------------------------
|
|
568
|
+
// Internal helpers
|
|
569
|
+
// ----------------------------
|
|
570
|
+
moduleContext() {
|
|
571
|
+
return {
|
|
572
|
+
wallet: this.wallet,
|
|
573
|
+
originator: this.cfg.originator,
|
|
574
|
+
now: this.now,
|
|
575
|
+
logger: this.cfg.logger
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
makeEnvelope(kind, threadId, payload) {
|
|
579
|
+
return {
|
|
580
|
+
v: 1,
|
|
581
|
+
id: this.threadIdFactory(),
|
|
582
|
+
kind,
|
|
583
|
+
threadId,
|
|
584
|
+
createdAt: this.now(),
|
|
585
|
+
payload
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
async sendEnvelope(recipient, env, hostOverride) {
|
|
589
|
+
const body = JSON.stringify(env);
|
|
590
|
+
// Prefer live if available.
|
|
591
|
+
if (typeof this.comms.sendLiveMessage === 'function') {
|
|
592
|
+
try {
|
|
593
|
+
const mid = await this.comms.sendLiveMessage({ recipient, messageBox: this.messageBox, body }, hostOverride);
|
|
594
|
+
this.emitEvent({ type: 'envelopeSent', threadId: env.threadId, envelope: env, transportMessageId: mid });
|
|
595
|
+
return mid;
|
|
596
|
+
}
|
|
597
|
+
catch (e) {
|
|
598
|
+
this.cfg.logger?.warn?.('[RemittanceManager] sendLiveMessage failed, falling back to non-live', e);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
const mid = await this.comms.sendMessage({ recipient, messageBox: this.messageBox, body }, hostOverride);
|
|
602
|
+
this.emitEvent({ type: 'envelopeSent', threadId: env.threadId, envelope: env, transportMessageId: mid });
|
|
603
|
+
return mid;
|
|
604
|
+
}
|
|
605
|
+
getOrCreateThreadFromInboundEnvelope(env, msg) {
|
|
606
|
+
const existing = this.getThread(env.threadId);
|
|
607
|
+
if (typeof existing === 'object')
|
|
608
|
+
return existing;
|
|
609
|
+
// If we didn't create the thread, infer roles from the first message kind:
|
|
610
|
+
// - Receiving identity verification request/response/acknowledgment -> we are either maker or taker depending on config
|
|
611
|
+
// - Receiving an invoice -> we are taker (payer)
|
|
612
|
+
// - Receiving a settlement -> we are maker (payee)
|
|
613
|
+
// - Receiving a receipt -> we are taker
|
|
614
|
+
// - Receiving a termination -> assume we are taker
|
|
615
|
+
const createdAt = this.now();
|
|
616
|
+
const inferredMyRole = (() => {
|
|
617
|
+
if (env.kind === 'invoice')
|
|
618
|
+
return 'taker';
|
|
619
|
+
if (env.kind === 'settlement')
|
|
620
|
+
return 'maker';
|
|
621
|
+
if (env.kind === 'receipt')
|
|
622
|
+
return 'taker';
|
|
623
|
+
if (env.kind === 'termination')
|
|
624
|
+
return 'taker';
|
|
625
|
+
if (env.kind === 'identityVerificationRequest' ||
|
|
626
|
+
env.kind === 'identityVerificationResponse' ||
|
|
627
|
+
env.kind === 'identityVerificationAcknowledgment') {
|
|
628
|
+
const makerRequest = this.runtime.identityOptions?.makerRequestIdentity ?? 'never';
|
|
629
|
+
const takerRequest = this.runtime.identityOptions?.takerRequestIdentity ?? 'never';
|
|
630
|
+
const makerRequests = makerRequest !== 'never';
|
|
631
|
+
const takerRequests = takerRequest !== 'never';
|
|
632
|
+
let requesterRole;
|
|
633
|
+
if (makerRequests && !takerRequests) {
|
|
634
|
+
requesterRole = 'maker';
|
|
635
|
+
}
|
|
636
|
+
else if (takerRequests && !makerRequests) {
|
|
637
|
+
requesterRole = 'taker';
|
|
638
|
+
}
|
|
639
|
+
else if (makerRequests && takerRequests && makerRequest !== takerRequest) {
|
|
640
|
+
requesterRole =
|
|
641
|
+
makerRequest === 'beforeInvoicing' && takerRequest === 'beforeSettlement'
|
|
642
|
+
? 'maker'
|
|
643
|
+
: makerRequest === 'beforeSettlement' && takerRequest === 'beforeInvoicing'
|
|
644
|
+
? 'taker'
|
|
645
|
+
: undefined;
|
|
646
|
+
}
|
|
647
|
+
if (typeof requesterRole !== 'string')
|
|
648
|
+
return 'taker';
|
|
649
|
+
if (env.kind === 'identityVerificationResponse') {
|
|
650
|
+
return requesterRole;
|
|
651
|
+
}
|
|
652
|
+
return requesterRole === 'maker' ? 'taker' : 'maker';
|
|
653
|
+
}
|
|
654
|
+
return 'taker';
|
|
655
|
+
})();
|
|
656
|
+
const inferredTheirRole = inferredMyRole === 'maker' ? 'taker' : 'maker';
|
|
657
|
+
const t = {
|
|
658
|
+
threadId: env.threadId,
|
|
659
|
+
counterparty: msg.sender,
|
|
660
|
+
myRole: inferredMyRole,
|
|
661
|
+
theirRole: inferredTheirRole,
|
|
662
|
+
createdAt,
|
|
663
|
+
updatedAt: createdAt,
|
|
664
|
+
state: 'new',
|
|
665
|
+
stateLog: [],
|
|
666
|
+
processedMessageIds: [],
|
|
667
|
+
protocolLog: [],
|
|
668
|
+
identity: {
|
|
669
|
+
certsSent: [],
|
|
670
|
+
certsReceived: [],
|
|
671
|
+
requestSent: false,
|
|
672
|
+
responseSent: false,
|
|
673
|
+
acknowledgmentSent: false,
|
|
674
|
+
acknowledgmentReceived: false
|
|
675
|
+
},
|
|
676
|
+
flags: {
|
|
677
|
+
hasIdentified: false,
|
|
678
|
+
hasInvoiced: false,
|
|
679
|
+
hasPaid: false,
|
|
680
|
+
hasReceipted: false,
|
|
681
|
+
error: false
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
this.threads.push(t);
|
|
685
|
+
this.emitEvent({ type: 'threadCreated', threadId: t.threadId, thread: t });
|
|
686
|
+
return t;
|
|
687
|
+
}
|
|
688
|
+
async handleInboundMessage(msg) {
|
|
689
|
+
const parsed = safeParseEnvelope(msg.body);
|
|
690
|
+
if (parsed == null) {
|
|
691
|
+
// Not our protocol message; leave it for the application or acknowledge? Here we leave it.
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
const thread = this.getOrCreateThreadFromInboundEnvelope(parsed, msg);
|
|
695
|
+
if (thread.processedMessageIds.includes(msg.messageId)) {
|
|
696
|
+
// Already applied; ack and continue.
|
|
697
|
+
await this.safeAck([msg.messageId]);
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
try {
|
|
701
|
+
await this.applyInboundEnvelope(thread, parsed, msg);
|
|
702
|
+
thread.processedMessageIds.push(msg.messageId);
|
|
703
|
+
thread.updatedAt = this.now();
|
|
704
|
+
await this.persistState();
|
|
705
|
+
await this.safeAck([msg.messageId]);
|
|
706
|
+
}
|
|
707
|
+
catch (e) {
|
|
708
|
+
this.markThreadError(thread, e);
|
|
709
|
+
await this.persistState();
|
|
710
|
+
// Do not acknowledge so it can be retried.
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
async applyInboundEnvelope(thread, env, msg) {
|
|
714
|
+
thread.protocolLog.push({ direction: 'in', envelope: env, transportMessageId: msg.messageId });
|
|
715
|
+
this.emitEvent({ type: 'envelopeReceived', threadId: thread.threadId, envelope: env, transportMessageId: msg.messageId });
|
|
716
|
+
switch (env.kind) {
|
|
717
|
+
case 'identityVerificationRequest': {
|
|
718
|
+
const payload = env.payload;
|
|
719
|
+
if (typeof payload !== 'object') {
|
|
720
|
+
throw new Error('Identity verification request payload missing data');
|
|
721
|
+
}
|
|
722
|
+
if (this.cfg.identityLayer == null) {
|
|
723
|
+
await this.sendTermination(thread, msg.sender, 'Identity verification requested but no identity layer is configured');
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
this.transitionThreadState(thread, 'identityRequested', 'identity request received');
|
|
727
|
+
this.emitEvent({ type: 'identityRequested', threadId: thread.threadId, direction: 'in', request: payload });
|
|
728
|
+
const response = await this.cfg.identityLayer.respondToRequest({ counterparty: msg.sender, threadId: thread.threadId, request: payload }, this.moduleContext());
|
|
729
|
+
if (response.action === 'terminate') {
|
|
730
|
+
await this.sendTermination(thread, msg.sender, response.termination.message, response.termination.details, response.termination.code);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
const responseEnv = this.makeEnvelope('identityVerificationResponse', thread.threadId, response.response);
|
|
734
|
+
const mid = await this.sendEnvelope(msg.sender, responseEnv);
|
|
735
|
+
thread.protocolLog.push({ direction: 'out', envelope: responseEnv, transportMessageId: mid });
|
|
736
|
+
thread.identity.certsSent = response.response.certificates;
|
|
737
|
+
thread.identity.responseSent = true;
|
|
738
|
+
this.transitionThreadState(thread, 'identityResponded', 'identity response sent');
|
|
739
|
+
this.emitEvent({ type: 'identityResponded', threadId: thread.threadId, direction: 'out', response: response.response });
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
case 'identityVerificationResponse': {
|
|
743
|
+
const payload = env.payload;
|
|
744
|
+
if (typeof payload !== 'object') {
|
|
745
|
+
throw new Error('Identity verification response payload missing data');
|
|
746
|
+
}
|
|
747
|
+
if (this.cfg.identityLayer == null) {
|
|
748
|
+
await this.sendTermination(thread, msg.sender, 'Identity verification response received but no identity layer is configured');
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
thread.identity.certsReceived = payload.certificates;
|
|
752
|
+
this.transitionThreadState(thread, 'identityResponded', 'identity response received');
|
|
753
|
+
this.emitEvent({ type: 'identityResponded', threadId: thread.threadId, direction: 'in', response: payload });
|
|
754
|
+
const decision = await this.cfg.identityLayer.assessReceivedCertificateSufficiency(msg.sender, payload, thread.threadId);
|
|
755
|
+
if ('message' in decision) {
|
|
756
|
+
await this.sendTermination(thread, msg.sender, decision.message, decision.details, decision.code);
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
if (decision.kind === 'identityVerificationAcknowledgment') {
|
|
760
|
+
const ackEnv = this.makeEnvelope('identityVerificationAcknowledgment', thread.threadId, decision);
|
|
761
|
+
const mid = await this.sendEnvelope(msg.sender, ackEnv);
|
|
762
|
+
thread.protocolLog.push({ direction: 'out', envelope: ackEnv, transportMessageId: mid });
|
|
763
|
+
thread.identity.acknowledgmentSent = true;
|
|
764
|
+
thread.flags.hasIdentified = true;
|
|
765
|
+
this.transitionThreadState(thread, 'identityAcknowledged', 'identity acknowledgment sent');
|
|
766
|
+
this.emitEvent({ type: 'identityAcknowledged', threadId: thread.threadId, direction: 'out', acknowledgment: decision });
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
throw new Error('Unknown identity verification decision');
|
|
770
|
+
}
|
|
771
|
+
case 'identityVerificationAcknowledgment': {
|
|
772
|
+
const payload = env.payload;
|
|
773
|
+
if (typeof payload !== 'object') {
|
|
774
|
+
throw new Error('Identity verification acknowledgment payload missing data');
|
|
775
|
+
}
|
|
776
|
+
thread.identity.acknowledgmentReceived = true;
|
|
777
|
+
thread.flags.hasIdentified = true;
|
|
778
|
+
this.transitionThreadState(thread, 'identityAcknowledged', 'identity acknowledgment received');
|
|
779
|
+
this.emitEvent({ type: 'identityAcknowledged', threadId: thread.threadId, direction: 'in', acknowledgment: payload });
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
case 'invoice': {
|
|
783
|
+
const invoice = env.payload;
|
|
784
|
+
if (typeof invoice !== 'object') {
|
|
785
|
+
throw new Error('Invoice payload missing invoice data');
|
|
786
|
+
}
|
|
787
|
+
thread.invoice = invoice;
|
|
788
|
+
thread.flags.hasInvoiced = true;
|
|
789
|
+
this.transitionThreadState(thread, 'invoiced', 'invoice received');
|
|
790
|
+
this.emitEvent({ type: 'invoiceReceived', threadId: thread.threadId, invoice });
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
case 'settlement': {
|
|
794
|
+
const settlement = env.payload;
|
|
795
|
+
if (typeof settlement !== 'object') {
|
|
796
|
+
throw new Error('Settlement payload missing settlement data');
|
|
797
|
+
}
|
|
798
|
+
if (this.shouldRequireIdentityBeforeSettlement(thread) && !thread.flags.hasIdentified) {
|
|
799
|
+
await this.sendTermination(thread, msg.sender, 'Identity verification is required before settlement');
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
// Persist settlement immediately (even if we later reject); it is part of the audit trail.
|
|
803
|
+
thread.settlement = settlement;
|
|
804
|
+
thread.flags.hasPaid = true;
|
|
805
|
+
this.transitionThreadState(thread, 'settled', 'settlement received');
|
|
806
|
+
this.emitEvent({ type: 'settlementReceived', threadId: thread.threadId, settlement });
|
|
807
|
+
const module = this.moduleRegistry.get(settlement.moduleId);
|
|
808
|
+
if (typeof module !== 'object') {
|
|
809
|
+
await this.maybeSendTermination(thread, settlement, msg.sender, `Unsupported module: ${settlement.moduleId}`);
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
if ((thread.invoice == null) && !module.allowUnsolicitedSettlements) {
|
|
813
|
+
await this.maybeSendTermination(thread, settlement, msg.sender, 'Unsolicited settlement not supported');
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
const result = await module.acceptSettlement({
|
|
817
|
+
threadId: thread.threadId,
|
|
818
|
+
invoice: thread.invoice,
|
|
819
|
+
settlement: settlement.artifact,
|
|
820
|
+
sender: msg.sender
|
|
821
|
+
}, this.moduleContext()).catch(async (e) => {
|
|
822
|
+
const errMsg = e instanceof Error ? e.message : String(e);
|
|
823
|
+
await this.maybeSendTermination(thread, settlement, msg.sender, `Settlement processing failed: ${errMsg}`);
|
|
824
|
+
throw e; // re-throw to stop further processing
|
|
825
|
+
});
|
|
826
|
+
if (result.action === 'accept') {
|
|
827
|
+
const myKey = this.requireMyIdentityKey('Receiving settlement requires identity key');
|
|
828
|
+
const payerKey = msg.sender;
|
|
829
|
+
const receipt = {
|
|
830
|
+
kind: 'receipt',
|
|
831
|
+
threadId: thread.threadId,
|
|
832
|
+
moduleId: settlement.moduleId,
|
|
833
|
+
optionId: settlement.optionId,
|
|
834
|
+
payee: myKey,
|
|
835
|
+
payer: payerKey,
|
|
836
|
+
createdAt: this.now(),
|
|
837
|
+
receiptData: result.receiptData
|
|
838
|
+
};
|
|
839
|
+
thread.receipt = receipt;
|
|
840
|
+
thread.flags.hasReceipted = true;
|
|
841
|
+
this.transitionThreadState(thread, 'receipted', 'receipt issued');
|
|
842
|
+
if (this.runtime.receiptProvided && this.runtime.autoIssueReceipt) {
|
|
843
|
+
const receiptEnv = this.makeEnvelope('receipt', thread.threadId, receipt);
|
|
844
|
+
const mid = await this.sendEnvelope(msg.sender, receiptEnv);
|
|
845
|
+
thread.protocolLog.push({ direction: 'out', envelope: receiptEnv, transportMessageId: mid });
|
|
846
|
+
this.emitEvent({ type: 'receiptSent', threadId: thread.threadId, receipt });
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
else if (result.action === 'terminate') {
|
|
850
|
+
await this.maybeSendTermination(thread, settlement, msg.sender, result.termination.message, result.termination.details);
|
|
851
|
+
}
|
|
852
|
+
else {
|
|
853
|
+
throw new Error('Unknown settlement acceptance action');
|
|
854
|
+
}
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
case 'receipt': {
|
|
858
|
+
const receipt = env.payload;
|
|
859
|
+
if (typeof receipt !== 'object') {
|
|
860
|
+
throw new Error('Receipt payload missing receipt data');
|
|
861
|
+
}
|
|
862
|
+
thread.receipt = receipt;
|
|
863
|
+
thread.flags.hasReceipted = true;
|
|
864
|
+
this.transitionThreadState(thread, 'receipted', 'receipt received');
|
|
865
|
+
this.emitEvent({ type: 'receiptReceived', threadId: thread.threadId, receipt });
|
|
866
|
+
const module = this.moduleRegistry.get(receipt.moduleId);
|
|
867
|
+
if (module?.processReceipt != null) {
|
|
868
|
+
await module.processReceipt({ threadId: thread.threadId, invoice: thread.invoice, receiptData: receipt.receiptData, sender: msg.sender }, this.moduleContext());
|
|
869
|
+
}
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
case 'termination': {
|
|
873
|
+
const payload = env.payload;
|
|
874
|
+
if (typeof payload !== 'object') {
|
|
875
|
+
throw new Error('Termination payload missing data');
|
|
876
|
+
}
|
|
877
|
+
thread.termination = payload;
|
|
878
|
+
thread.lastError = { message: payload.message, at: this.now() };
|
|
879
|
+
thread.flags.error = true;
|
|
880
|
+
this.transitionThreadState(thread, 'terminated', 'termination received');
|
|
881
|
+
this.emitEvent({ type: 'terminationReceived', threadId: thread.threadId, termination: payload });
|
|
882
|
+
if (thread.settlement != null) {
|
|
883
|
+
const module = this.moduleRegistry.get(thread.settlement.moduleId);
|
|
884
|
+
if ((module?.processTermination) != null) {
|
|
885
|
+
await module.processTermination({ threadId: thread.threadId, invoice: thread.invoice, settlement: thread.settlement, termination: payload, sender: msg.sender }, this.moduleContext());
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
default: {
|
|
891
|
+
const kind = env.kind;
|
|
892
|
+
throw new Error(`Unknown envelope kind: ${String(kind)}`);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
async maybeSendTermination(thread, settlement, payer, message, details) {
|
|
897
|
+
const t = {
|
|
898
|
+
code: 'error',
|
|
899
|
+
message,
|
|
900
|
+
details
|
|
901
|
+
};
|
|
902
|
+
const env = this.makeEnvelope('termination', thread.threadId, t);
|
|
903
|
+
const mid = await this.sendEnvelope(payer, env);
|
|
904
|
+
thread.protocolLog.push({ direction: 'out', envelope: env, transportMessageId: mid });
|
|
905
|
+
this.emitEvent({ type: 'terminationSent', threadId: thread.threadId, termination: t });
|
|
906
|
+
thread.termination = t;
|
|
907
|
+
thread.lastError = {
|
|
908
|
+
message: `Sent termination: ${message}`,
|
|
909
|
+
at: this.now()
|
|
910
|
+
};
|
|
911
|
+
thread.flags.error = true;
|
|
912
|
+
this.transitionThreadState(thread, 'terminated', 'termination sent');
|
|
913
|
+
}
|
|
914
|
+
async sendTermination(thread, recipient, message, details, code = 'error') {
|
|
915
|
+
const t = { code, message, details };
|
|
916
|
+
const env = this.makeEnvelope('termination', thread.threadId, t);
|
|
917
|
+
const mid = await this.sendEnvelope(recipient, env);
|
|
918
|
+
thread.protocolLog.push({ direction: 'out', envelope: env, transportMessageId: mid });
|
|
919
|
+
this.emitEvent({ type: 'terminationSent', threadId: thread.threadId, termination: t });
|
|
920
|
+
thread.termination = t;
|
|
921
|
+
thread.lastError = { message: `Sent termination: ${message}`, at: this.now() };
|
|
922
|
+
thread.flags.error = true;
|
|
923
|
+
this.transitionThreadState(thread, 'terminated', 'termination sent');
|
|
924
|
+
}
|
|
925
|
+
shouldRequestIdentity(thread, phase) {
|
|
926
|
+
const { makerRequestIdentity = 'never', takerRequestIdentity = 'never' } = this.runtime.identityOptions ?? {};
|
|
927
|
+
const requiresIdentity = thread.myRole === 'maker' ? makerRequestIdentity === phase : takerRequestIdentity === phase;
|
|
928
|
+
if (!requiresIdentity)
|
|
929
|
+
return false;
|
|
930
|
+
if (this.cfg.identityLayer == null) {
|
|
931
|
+
throw new Error('Identity layer is required by runtime options but is not configured');
|
|
932
|
+
}
|
|
933
|
+
return true;
|
|
934
|
+
}
|
|
935
|
+
shouldRequireIdentityBeforeSettlement(thread) {
|
|
936
|
+
if (thread.myRole !== 'maker')
|
|
937
|
+
return false;
|
|
938
|
+
return (this.runtime.identityOptions?.makerRequestIdentity ?? 'never') === 'beforeSettlement';
|
|
939
|
+
}
|
|
940
|
+
async ensureIdentityExchange(thread, hostOverride) {
|
|
941
|
+
if (this.cfg.identityLayer == null)
|
|
942
|
+
return;
|
|
943
|
+
if (thread.flags.hasIdentified)
|
|
944
|
+
return;
|
|
945
|
+
if (!thread.identity.requestSent) {
|
|
946
|
+
const request = await this.cfg.identityLayer.determineCertificatesToRequest({ counterparty: thread.counterparty, threadId: thread.threadId }, this.moduleContext());
|
|
947
|
+
const env = this.makeEnvelope('identityVerificationRequest', thread.threadId, request);
|
|
948
|
+
const mid = await this.sendEnvelope(thread.counterparty, env, hostOverride);
|
|
949
|
+
thread.protocolLog.push({ direction: 'out', envelope: env, transportMessageId: mid });
|
|
950
|
+
thread.identity.requestSent = true;
|
|
951
|
+
this.transitionThreadState(thread, 'identityRequested', 'identity request sent');
|
|
952
|
+
this.emitEvent({ type: 'identityRequested', threadId: thread.threadId, direction: 'out', request });
|
|
953
|
+
thread.updatedAt = this.now();
|
|
954
|
+
await this.persistState();
|
|
955
|
+
}
|
|
956
|
+
await this.waitForIdentityAcknowledgment(thread.threadId, {
|
|
957
|
+
timeoutMs: this.runtime.identityTimeoutMs,
|
|
958
|
+
pollIntervalMs: this.runtime.identityPollIntervalMs
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
async waitForIdentityAcknowledgment(threadId, opts = {}) {
|
|
962
|
+
await this.waitForState(threadId, 'identityAcknowledged', opts);
|
|
963
|
+
}
|
|
964
|
+
async safeAck(messageIds) {
|
|
965
|
+
try {
|
|
966
|
+
await this.comms.acknowledgeMessage({ messageIds });
|
|
967
|
+
}
|
|
968
|
+
catch (e) {
|
|
969
|
+
this.cfg.logger?.warn?.('[RemittanceManager] Failed to acknowledge message(s)', e);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
markThreadError(thread, e) {
|
|
973
|
+
thread.flags.error = true;
|
|
974
|
+
this.transitionThreadState(thread, 'errored', 'thread error');
|
|
975
|
+
thread.lastError = { message: String(e?.message ?? e), at: this.now() };
|
|
976
|
+
this.cfg.logger?.error?.('[RemittanceManager] Thread error', thread.threadId, e);
|
|
977
|
+
this.emitEvent({ type: 'error', threadId: thread.threadId, error: String(e?.message ?? e) });
|
|
978
|
+
}
|
|
979
|
+
ensureThreadState(thread) {
|
|
980
|
+
var _a, _b, _c, _d, _e, _f;
|
|
981
|
+
thread.identity = thread.identity ?? {
|
|
982
|
+
certsSent: [],
|
|
983
|
+
certsReceived: [],
|
|
984
|
+
requestSent: false,
|
|
985
|
+
responseSent: false,
|
|
986
|
+
acknowledgmentSent: false,
|
|
987
|
+
acknowledgmentReceived: false
|
|
988
|
+
};
|
|
989
|
+
(_a = thread.identity).certsSent ?? (_a.certsSent = []);
|
|
990
|
+
(_b = thread.identity).certsReceived ?? (_b.certsReceived = []);
|
|
991
|
+
(_c = thread.identity).requestSent ?? (_c.requestSent = false);
|
|
992
|
+
(_d = thread.identity).responseSent ?? (_d.responseSent = false);
|
|
993
|
+
(_e = thread.identity).acknowledgmentSent ?? (_e.acknowledgmentSent = false);
|
|
994
|
+
(_f = thread.identity).acknowledgmentReceived ?? (_f.acknowledgmentReceived = false);
|
|
995
|
+
thread.flags = thread.flags ?? {
|
|
996
|
+
hasIdentified: false,
|
|
997
|
+
hasInvoiced: false,
|
|
998
|
+
hasPaid: false,
|
|
999
|
+
hasReceipted: false,
|
|
1000
|
+
error: false
|
|
1001
|
+
};
|
|
1002
|
+
thread.processedMessageIds ?? (thread.processedMessageIds = []);
|
|
1003
|
+
thread.protocolLog ?? (thread.protocolLog = []);
|
|
1004
|
+
thread.stateLog ?? (thread.stateLog = []);
|
|
1005
|
+
if (thread.state == null) {
|
|
1006
|
+
thread.state = this.deriveThreadState(thread);
|
|
1007
|
+
}
|
|
1008
|
+
return thread;
|
|
1009
|
+
}
|
|
1010
|
+
deriveThreadState(thread) {
|
|
1011
|
+
if (thread.flags.error)
|
|
1012
|
+
return 'errored';
|
|
1013
|
+
if (thread.termination != null)
|
|
1014
|
+
return 'terminated';
|
|
1015
|
+
if (thread.receipt != null)
|
|
1016
|
+
return 'receipted';
|
|
1017
|
+
if (thread.settlement != null)
|
|
1018
|
+
return 'settled';
|
|
1019
|
+
if (thread.invoice != null)
|
|
1020
|
+
return 'invoiced';
|
|
1021
|
+
if (thread.identity.acknowledgmentReceived || thread.identity.acknowledgmentSent || thread.flags.hasIdentified) {
|
|
1022
|
+
return 'identityAcknowledged';
|
|
1023
|
+
}
|
|
1024
|
+
if (thread.identity.responseSent || thread.identity.certsSent.length > 0)
|
|
1025
|
+
return 'identityResponded';
|
|
1026
|
+
if (thread.identity.requestSent || thread.identity.certsReceived.length > 0)
|
|
1027
|
+
return 'identityRequested';
|
|
1028
|
+
return 'new';
|
|
1029
|
+
}
|
|
1030
|
+
transitionThreadState(thread, next, reason) {
|
|
1031
|
+
const current = thread.state;
|
|
1032
|
+
if (current === next)
|
|
1033
|
+
return;
|
|
1034
|
+
const allowed = types_js_1.REMITTANCE_STATE_TRANSITIONS[current] ?? [];
|
|
1035
|
+
if (!allowed.includes(next)) {
|
|
1036
|
+
throw new Error(`Invalid remittance state transition: ${current} -> ${next}`);
|
|
1037
|
+
}
|
|
1038
|
+
thread.state = next;
|
|
1039
|
+
thread.updatedAt = this.now();
|
|
1040
|
+
thread.stateLog.push({ at: this.now(), from: current, to: next, reason });
|
|
1041
|
+
this.emitEvent({ type: 'stateChanged', threadId: thread.threadId, previous: current, next, reason });
|
|
1042
|
+
this.resolveStateWaiters(thread.threadId, next);
|
|
1043
|
+
if (next === 'terminated' || next === 'errored') {
|
|
1044
|
+
this.rejectStateWaiters(thread.threadId, new Error(`Thread entered terminal state: ${next}`));
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
resolveStateWaiters(threadId, state) {
|
|
1048
|
+
const waiters = this.stateWaiters.get(threadId);
|
|
1049
|
+
if (waiters == null)
|
|
1050
|
+
return;
|
|
1051
|
+
const remaining = [];
|
|
1052
|
+
for (const waiter of waiters) {
|
|
1053
|
+
if (waiter.state === state) {
|
|
1054
|
+
waiter.resolve();
|
|
1055
|
+
}
|
|
1056
|
+
else {
|
|
1057
|
+
remaining.push(waiter);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
if (remaining.length === 0) {
|
|
1061
|
+
this.stateWaiters.delete(threadId);
|
|
1062
|
+
}
|
|
1063
|
+
else {
|
|
1064
|
+
this.stateWaiters.set(threadId, remaining);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
rejectStateWaiters(threadId, err) {
|
|
1068
|
+
const waiters = this.stateWaiters.get(threadId);
|
|
1069
|
+
if (waiters == null)
|
|
1070
|
+
return;
|
|
1071
|
+
for (const waiter of waiters) {
|
|
1072
|
+
waiter.reject(err);
|
|
1073
|
+
}
|
|
1074
|
+
this.stateWaiters.delete(threadId);
|
|
1075
|
+
}
|
|
1076
|
+
emitEvent(event) {
|
|
1077
|
+
const handlers = this.eventHandlers;
|
|
1078
|
+
if (handlers != null) {
|
|
1079
|
+
try {
|
|
1080
|
+
switch (event.type) {
|
|
1081
|
+
case 'threadCreated':
|
|
1082
|
+
handlers.onThreadCreated?.(event);
|
|
1083
|
+
break;
|
|
1084
|
+
case 'stateChanged':
|
|
1085
|
+
handlers.onStateChanged?.(event);
|
|
1086
|
+
break;
|
|
1087
|
+
case 'envelopeSent':
|
|
1088
|
+
handlers.onEnvelopeSent?.(event);
|
|
1089
|
+
break;
|
|
1090
|
+
case 'envelopeReceived':
|
|
1091
|
+
handlers.onEnvelopeReceived?.(event);
|
|
1092
|
+
break;
|
|
1093
|
+
case 'identityRequested':
|
|
1094
|
+
handlers.onIdentityRequested?.(event);
|
|
1095
|
+
break;
|
|
1096
|
+
case 'identityResponded':
|
|
1097
|
+
handlers.onIdentityResponded?.(event);
|
|
1098
|
+
break;
|
|
1099
|
+
case 'identityAcknowledged':
|
|
1100
|
+
handlers.onIdentityAcknowledged?.(event);
|
|
1101
|
+
break;
|
|
1102
|
+
case 'invoiceSent':
|
|
1103
|
+
handlers.onInvoiceSent?.(event);
|
|
1104
|
+
break;
|
|
1105
|
+
case 'invoiceReceived':
|
|
1106
|
+
handlers.onInvoiceReceived?.(event);
|
|
1107
|
+
break;
|
|
1108
|
+
case 'settlementSent':
|
|
1109
|
+
handlers.onSettlementSent?.(event);
|
|
1110
|
+
break;
|
|
1111
|
+
case 'settlementReceived':
|
|
1112
|
+
handlers.onSettlementReceived?.(event);
|
|
1113
|
+
break;
|
|
1114
|
+
case 'receiptSent':
|
|
1115
|
+
handlers.onReceiptSent?.(event);
|
|
1116
|
+
break;
|
|
1117
|
+
case 'receiptReceived':
|
|
1118
|
+
handlers.onReceiptReceived?.(event);
|
|
1119
|
+
break;
|
|
1120
|
+
case 'terminationSent':
|
|
1121
|
+
handlers.onTerminationSent?.(event);
|
|
1122
|
+
break;
|
|
1123
|
+
case 'terminationReceived':
|
|
1124
|
+
handlers.onTerminationReceived?.(event);
|
|
1125
|
+
break;
|
|
1126
|
+
case 'error':
|
|
1127
|
+
handlers.onError?.(event);
|
|
1128
|
+
break;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
catch (e) {
|
|
1132
|
+
this.cfg.logger?.warn?.('[RemittanceManager] Event handler error', e);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
for (const listener of this.eventListeners) {
|
|
1136
|
+
try {
|
|
1137
|
+
listener(event);
|
|
1138
|
+
}
|
|
1139
|
+
catch (e) {
|
|
1140
|
+
this.cfg.logger?.warn?.('[RemittanceManager] Event listener error', e);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
async refreshMyIdentityKey() {
|
|
1145
|
+
if (typeof this.myIdentityKey === 'string')
|
|
1146
|
+
return;
|
|
1147
|
+
if (typeof this.wallet !== 'object')
|
|
1148
|
+
return;
|
|
1149
|
+
const { publicKey: k } = await this.wallet.getPublicKey({ identityKey: true }, this.cfg.originator);
|
|
1150
|
+
if (typeof k === 'string' && k.trim() !== '') {
|
|
1151
|
+
this.myIdentityKey = k;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
requireMyIdentityKey(errMsg) {
|
|
1155
|
+
if (typeof this.myIdentityKey !== 'string') {
|
|
1156
|
+
throw new Error(errMsg);
|
|
1157
|
+
}
|
|
1158
|
+
return this.myIdentityKey;
|
|
1159
|
+
}
|
|
1160
|
+
async composeInvoice(threadId, payee, payer, input) {
|
|
1161
|
+
const createdAt = this.now();
|
|
1162
|
+
const expiresAt = this.runtime.invoiceExpirySeconds >= 0 ? createdAt + this.runtime.invoiceExpirySeconds * 1000 : undefined;
|
|
1163
|
+
return {
|
|
1164
|
+
kind: 'invoice',
|
|
1165
|
+
threadId,
|
|
1166
|
+
payee,
|
|
1167
|
+
payer,
|
|
1168
|
+
note: input.note,
|
|
1169
|
+
lineItems: input.lineItems,
|
|
1170
|
+
total: input.total,
|
|
1171
|
+
invoiceNumber: input.invoiceNumber ?? threadId,
|
|
1172
|
+
createdAt,
|
|
1173
|
+
expiresAt,
|
|
1174
|
+
arbitrary: input.arbitrary,
|
|
1175
|
+
options: {}
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
exports.RemittanceManager = RemittanceManager;
|
|
1180
|
+
/**
|
|
1181
|
+
* A lightweight wrapper around a thread's invoice, with convenience methods.
|
|
1182
|
+
*/
|
|
1183
|
+
class ThreadHandle {
|
|
1184
|
+
constructor(manager, threadId) {
|
|
1185
|
+
this.manager = manager;
|
|
1186
|
+
this.threadId = threadId;
|
|
1187
|
+
}
|
|
1188
|
+
get thread() {
|
|
1189
|
+
return this.manager.getThreadOrThrow(this.threadId);
|
|
1190
|
+
}
|
|
1191
|
+
async waitForState(state, opts) {
|
|
1192
|
+
return await this.manager.waitForState(this.threadId, state, opts);
|
|
1193
|
+
}
|
|
1194
|
+
async waitForIdentity(opts) {
|
|
1195
|
+
return await this.manager.waitForIdentity(this.threadId, opts);
|
|
1196
|
+
}
|
|
1197
|
+
async waitForSettlement(opts) {
|
|
1198
|
+
return await this.manager.waitForSettlement(this.threadId, opts);
|
|
1199
|
+
}
|
|
1200
|
+
async waitForReceipt(opts) {
|
|
1201
|
+
return await this.manager.waitForReceipt(this.threadId, opts);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
exports.ThreadHandle = ThreadHandle;
|
|
1205
|
+
class InvoiceHandle extends ThreadHandle {
|
|
1206
|
+
get invoice() {
|
|
1207
|
+
const inv = this.thread.invoice;
|
|
1208
|
+
if (typeof inv !== 'object')
|
|
1209
|
+
throw new Error('Thread has no invoice');
|
|
1210
|
+
return inv;
|
|
1211
|
+
}
|
|
1212
|
+
/**
|
|
1213
|
+
* Pays the invoice using the selected remittance option.
|
|
1214
|
+
*/
|
|
1215
|
+
async pay(optionId) {
|
|
1216
|
+
return await this.manager.pay(this.threadId, optionId);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
exports.InvoiceHandle = InvoiceHandle;
|
|
1220
|
+
function safeParseEnvelope(body) {
|
|
1221
|
+
try {
|
|
1222
|
+
const parsed = JSON.parse(body);
|
|
1223
|
+
if (typeof parsed !== 'object')
|
|
1224
|
+
return undefined;
|
|
1225
|
+
if (parsed.v !== 1)
|
|
1226
|
+
return undefined;
|
|
1227
|
+
if (typeof parsed.kind !== 'string')
|
|
1228
|
+
return undefined;
|
|
1229
|
+
if (typeof parsed.threadId !== 'string')
|
|
1230
|
+
return undefined;
|
|
1231
|
+
if (typeof parsed.id !== 'string')
|
|
1232
|
+
return undefined;
|
|
1233
|
+
return parsed;
|
|
1234
|
+
}
|
|
1235
|
+
catch {
|
|
1236
|
+
return undefined;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
function defaultThreadIdFactory() {
|
|
1240
|
+
return (0, utils_js_1.toBase64)((0, Random_js_1.default)(32));
|
|
1241
|
+
}
|
|
1242
|
+
async function sleep(ms) {
|
|
1243
|
+
return await new Promise((resolve) => setTimeout(resolve, ms));
|
|
1244
|
+
}
|
|
1245
|
+
//# sourceMappingURL=RemittanceManager.js.map
|