@obbyx/x402-obyte 0.2.1 → 0.2.2
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/batch/client.d.ts +17 -0
- package/dist/batch/client.d.ts.map +1 -1
- package/dist/batch/client.js +54 -34
- package/dist/batch/client.js.map +1 -1
- package/dist/batch/facilitator.d.ts +14 -0
- package/dist/batch/facilitator.d.ts.map +1 -1
- package/dist/batch/facilitator.js +95 -75
- package/dist/batch/facilitator.js.map +1 -1
- package/dist/exact/client.d.ts +7 -0
- package/dist/exact/client.d.ts.map +1 -1
- package/dist/exact/client.js +31 -18
- package/dist/exact/client.js.map +1 -1
- package/dist/exact/facilitator.d.ts +2 -2
- package/dist/exact/facilitator.d.ts.map +1 -1
- package/dist/exact/facilitator.js +3 -3
- package/dist/exact/facilitator.js.map +1 -1
- package/dist/exact/ledger-checks.d.ts +1 -2
- package/dist/exact/ledger-checks.d.ts.map +1 -1
- package/dist/exact/ledger-checks.js +32 -8
- package/dist/exact/ledger-checks.js.map +1 -1
- package/dist/hub/schemas.d.ts +10 -0
- package/dist/hub/schemas.d.ts.map +1 -1
- package/dist/hub/schemas.js +14 -0
- package/dist/hub/schemas.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/batch/client.ts +76 -43
- package/src/batch/facilitator.ts +110 -84
- package/src/exact/client.ts +43 -22
- package/src/exact/facilitator.ts +2 -4
- package/src/exact/ledger-checks.ts +46 -9
- package/src/hub/schemas.ts +29 -0
- package/src/index.ts +2 -0
package/src/batch/facilitator.ts
CHANGED
|
@@ -186,8 +186,7 @@ export class BatchObyteFacilitator implements SchemeNetworkFacilitator {
|
|
|
186
186
|
channelKey: string,
|
|
187
187
|
network: Network,
|
|
188
188
|
): Promise<SettleResponse> {
|
|
189
|
-
|
|
190
|
-
let commitment = await store.commitmentByHash(c.packageHash);
|
|
189
|
+
let commitment = await this.options.store.commitmentByHash(c.packageHash);
|
|
191
190
|
if (commitment !== undefined && commitment.binding !== c.voucher.binding) {
|
|
192
191
|
return failure(
|
|
193
192
|
network,
|
|
@@ -196,96 +195,123 @@ export class BatchObyteFacilitator implements SchemeNetworkFacilitator {
|
|
|
196
195
|
);
|
|
197
196
|
}
|
|
198
197
|
if (commitment === undefined) {
|
|
199
|
-
const appended = await
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
aa: c.voucher.aa_address,
|
|
203
|
-
payTo: c.req.payTo,
|
|
204
|
-
payer: c.payer,
|
|
205
|
-
asset: c.req.asset,
|
|
206
|
-
period: c.voucher.period,
|
|
207
|
-
amountSpent: BigInt(c.voucher.amount_spent),
|
|
208
|
-
paymentAmount: BigInt(c.voucher.payment_amount),
|
|
209
|
-
binding: c.voucher.binding,
|
|
210
|
-
packageHash: c.packageHash,
|
|
211
|
-
packageJson: c.packageJson,
|
|
212
|
-
});
|
|
213
|
-
if (!appended.ok) {
|
|
214
|
-
const reason =
|
|
215
|
-
appended.reason === 'channel_busy'
|
|
216
|
-
? R.busy
|
|
217
|
-
: appended.reason === 'period_mismatch'
|
|
218
|
-
? R.period
|
|
219
|
-
: R.cumulative;
|
|
220
|
-
await this.emit({
|
|
221
|
-
key: channelKey,
|
|
222
|
-
kind: 'voucher_refused',
|
|
223
|
-
data: {
|
|
224
|
-
reason,
|
|
225
|
-
amountSpent: c.voucher.amount_spent,
|
|
226
|
-
committed: appended.committed.toString(),
|
|
227
|
-
},
|
|
228
|
-
});
|
|
229
|
-
return {
|
|
230
|
-
...failure(network, reason, `Voucher rejected: ${appended.reason}`),
|
|
231
|
-
extra: { chargedCumulativeAmount: appended.committed.toString() },
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
commitment = appended.commitment;
|
|
235
|
-
await this.emit({
|
|
236
|
-
key: channelKey,
|
|
237
|
-
kind: 'voucher_pending',
|
|
238
|
-
data: {
|
|
239
|
-
seq: commitment.seq,
|
|
240
|
-
period: commitment.period,
|
|
241
|
-
amountSpent: commitment.amountSpent.toString(),
|
|
242
|
-
},
|
|
243
|
-
});
|
|
244
|
-
this.log.info(
|
|
245
|
-
{ seq: commitment.seq, amountSpent: commitment.amountSpent.toString() },
|
|
246
|
-
'voucher pending',
|
|
247
|
-
);
|
|
198
|
+
const appended = await this.append(c, channelKey, network);
|
|
199
|
+
if ('success' in appended) return appended;
|
|
200
|
+
commitment = appended;
|
|
248
201
|
}
|
|
249
|
-
|
|
250
202
|
if (commitment.state === 'pending') {
|
|
251
|
-
const
|
|
252
|
-
if (
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
203
|
+
const committed = await this.handToWatcher(c, commitment, channelKey, network);
|
|
204
|
+
if ('success' in committed) return committed;
|
|
205
|
+
commitment = committed;
|
|
206
|
+
}
|
|
207
|
+
return this.success(network, c, commitment);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Appends the voucher to the channel journal as pending. The store enforces
|
|
212
|
+
* committed + price, the period and one voucher in flight per channel; a
|
|
213
|
+
* refusal reports the committed amount so an honest client can resync.
|
|
214
|
+
*/
|
|
215
|
+
private async append(
|
|
216
|
+
c: Authenticated,
|
|
217
|
+
channelKey: string,
|
|
218
|
+
network: Network,
|
|
219
|
+
): Promise<CommitmentRow | SettleResponse> {
|
|
220
|
+
const appended = await this.options.store.appendPending({
|
|
221
|
+
channelKey,
|
|
222
|
+
network: c.req.network.caip2,
|
|
223
|
+
aa: c.voucher.aa_address,
|
|
224
|
+
payTo: c.req.payTo,
|
|
225
|
+
payer: c.payer,
|
|
226
|
+
asset: c.req.asset,
|
|
227
|
+
period: c.voucher.period,
|
|
228
|
+
amountSpent: BigInt(c.voucher.amount_spent),
|
|
229
|
+
paymentAmount: BigInt(c.voucher.payment_amount),
|
|
230
|
+
binding: c.voucher.binding,
|
|
231
|
+
packageHash: c.packageHash,
|
|
232
|
+
packageJson: c.packageJson,
|
|
233
|
+
});
|
|
234
|
+
if (!appended.ok) {
|
|
235
|
+
const reason =
|
|
236
|
+
appended.reason === 'channel_busy'
|
|
237
|
+
? R.busy
|
|
238
|
+
: appended.reason === 'period_mismatch'
|
|
239
|
+
? R.period
|
|
240
|
+
: R.cumulative;
|
|
278
241
|
await this.emit({
|
|
279
242
|
key: channelKey,
|
|
280
|
-
kind: '
|
|
243
|
+
kind: 'voucher_refused',
|
|
281
244
|
data: {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
245
|
+
reason,
|
|
246
|
+
amountSpent: c.voucher.amount_spent,
|
|
247
|
+
committed: appended.committed.toString(),
|
|
285
248
|
},
|
|
286
249
|
});
|
|
250
|
+
return {
|
|
251
|
+
...failure(network, reason, `Voucher rejected: ${appended.reason}`),
|
|
252
|
+
extra: { chargedCumulativeAmount: appended.committed.toString() },
|
|
253
|
+
};
|
|
287
254
|
}
|
|
288
|
-
|
|
255
|
+
const commitment = appended.commitment;
|
|
256
|
+
await this.emit({
|
|
257
|
+
key: channelKey,
|
|
258
|
+
kind: 'voucher_pending',
|
|
259
|
+
data: {
|
|
260
|
+
seq: commitment.seq,
|
|
261
|
+
period: commitment.period,
|
|
262
|
+
amountSpent: commitment.amountSpent.toString(),
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
this.log.info(
|
|
266
|
+
{ seq: commitment.seq, amountSpent: commitment.amountSpent.toString() },
|
|
267
|
+
'voucher pending',
|
|
268
|
+
);
|
|
269
|
+
return commitment;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Delivers a pending voucher to the seller's watcher and commits it on its
|
|
274
|
+
* ack. Without the ack the voucher is stored here but not confirmed by the
|
|
275
|
+
* watcher: the outcome is unknown to the client → settlement_pending (x402:
|
|
276
|
+
* non-empty transaction), retried with the SAME voucher. Distinct from the
|
|
277
|
+
* definite refusal `…_watcher_unavailable` at verification.
|
|
278
|
+
*/
|
|
279
|
+
private async handToWatcher(
|
|
280
|
+
c: Authenticated,
|
|
281
|
+
pending: CommitmentRow,
|
|
282
|
+
channelKey: string,
|
|
283
|
+
network: Network,
|
|
284
|
+
): Promise<CommitmentRow | SettleResponse> {
|
|
285
|
+
const delivered = await this.options.gateway.deliver(pending, this.ackTimeoutMs);
|
|
286
|
+
if (delivered !== 'acked') {
|
|
287
|
+
this.log.warn({ seq: pending.seq }, 'watcher did not ack in time; voucher stays pending');
|
|
288
|
+
await this.emit({ key: channelKey, kind: 'ack_timeout', data: { seq: pending.seq } });
|
|
289
|
+
return {
|
|
290
|
+
success: false,
|
|
291
|
+
errorReason: 'settlement_pending',
|
|
292
|
+
errorMessage:
|
|
293
|
+
"Waiting for the seller's watcher to store this voucher; retry with the same voucher",
|
|
294
|
+
transaction: commitmentIdOf(network, pending),
|
|
295
|
+
network,
|
|
296
|
+
payer: c.payer,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
const commitment =
|
|
300
|
+
(await this.options.store.commit(pending.seq, {
|
|
301
|
+
payTo: pending.payTo,
|
|
302
|
+
network: pending.network,
|
|
303
|
+
})) ?? pending;
|
|
304
|
+
this.log.info({ seq: commitment.seq }, 'voucher committed (watcher ack)');
|
|
305
|
+
await this.emit({
|
|
306
|
+
key: channelKey,
|
|
307
|
+
kind: 'voucher_committed',
|
|
308
|
+
data: {
|
|
309
|
+
seq: commitment.seq,
|
|
310
|
+
period: commitment.period,
|
|
311
|
+
amountSpent: commitment.amountSpent.toString(),
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
return commitment;
|
|
289
315
|
}
|
|
290
316
|
|
|
291
317
|
private success(
|
package/src/exact/client.ts
CHANGED
|
@@ -9,7 +9,11 @@ import { type Logger, moduleLogger } from '@obbyx/obyte/logger';
|
|
|
9
9
|
import type obyte from 'obyte';
|
|
10
10
|
import type { ReservingComposer } from '../coins/index.js';
|
|
11
11
|
import { type PaymentRequirementsV1, requirementsV1ToV2 } from './facilitator-v1.js';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
EXACT_SCHEME,
|
|
14
|
+
type ParsedExactRequirements,
|
|
15
|
+
parseExactRequirements,
|
|
16
|
+
} from './requirements.js';
|
|
13
17
|
|
|
14
18
|
/**
|
|
15
19
|
* Client side of `exact` on Obyte: composes and signs (but does NOT
|
|
@@ -55,6 +59,36 @@ export class ExactObyteClient implements SchemeNetworkClient {
|
|
|
55
59
|
requirements: PaymentRequirements,
|
|
56
60
|
context?: PaymentPayloadContext,
|
|
57
61
|
): Promise<PaymentPayloadResult> {
|
|
62
|
+
const req = this.acceptable(x402Version, requirements, context);
|
|
63
|
+
const unit = await this.signUnit(req);
|
|
64
|
+
this.log.info(
|
|
65
|
+
{
|
|
66
|
+
proofId: unit['unit'],
|
|
67
|
+
payTo: req.payTo,
|
|
68
|
+
asset: req.asset,
|
|
69
|
+
amount: req.amount.toString(),
|
|
70
|
+
binding: req.extra.binding,
|
|
71
|
+
},
|
|
72
|
+
'payment unit signed (not broadcast)',
|
|
73
|
+
);
|
|
74
|
+
// v1: the mechanism returns the complete payload (core passes it through as-is).
|
|
75
|
+
if (x402Version === 1) {
|
|
76
|
+
return {
|
|
77
|
+
x402Version: 1,
|
|
78
|
+
scheme: EXACT_SCHEME,
|
|
79
|
+
network: requirements.network,
|
|
80
|
+
payload: { unit },
|
|
81
|
+
} as unknown as PaymentPayloadResult;
|
|
82
|
+
}
|
|
83
|
+
return { x402Version, payload: { unit } };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The requirements, if this client may pay them (network, limits, offer still valid). */
|
|
87
|
+
private acceptable(
|
|
88
|
+
x402Version: number,
|
|
89
|
+
requirements: PaymentRequirements,
|
|
90
|
+
context: PaymentPayloadContext | undefined,
|
|
91
|
+
): ParsedExactRequirements {
|
|
58
92
|
if (x402Version !== 1 && x402Version !== 2) {
|
|
59
93
|
throw new ObbyxError('invalid_x402_version', 'Only x402 v1 and v2 are supported');
|
|
60
94
|
}
|
|
@@ -85,7 +119,14 @@ export class ExactObyteClient implements SchemeNetworkClient {
|
|
|
85
119
|
if (req.extra.validUntil < Math.floor(Date.now() / 1000)) {
|
|
86
120
|
throw new ObbyxError('quote_expired', 'The payment offer has expired');
|
|
87
121
|
}
|
|
122
|
+
return req;
|
|
123
|
+
}
|
|
88
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Composes and signs (never broadcasts) the unit: one output to payTo and the
|
|
127
|
+
* binding in a data message. Refuses it when the network fee is above the limit.
|
|
128
|
+
*/
|
|
129
|
+
private async signUnit(req: ParsedExactRequirements): Promise<Record<string, unknown>> {
|
|
89
130
|
const output = { address: req.payTo, amount: toSafeNumber(req.amount) };
|
|
90
131
|
const payment =
|
|
91
132
|
req.asset === 'base'
|
|
@@ -95,7 +136,6 @@ export class ExactObyteClient implements SchemeNetworkClient {
|
|
|
95
136
|
app: 'data',
|
|
96
137
|
payload: { x402: { binding: req.extra.binding, quoteId: req.extra.quoteId } },
|
|
97
138
|
};
|
|
98
|
-
|
|
99
139
|
const compose = () =>
|
|
100
140
|
this.options.client.compose.message('multi', [payment, data] as never, {
|
|
101
141
|
privateKey: this.options.privateKey,
|
|
@@ -120,25 +160,6 @@ export class ExactObyteClient implements SchemeNetworkClient {
|
|
|
120
160
|
`Network fee ${fee} exceeds the limit ${this.options.maxFee} bytes`,
|
|
121
161
|
);
|
|
122
162
|
}
|
|
123
|
-
|
|
124
|
-
{
|
|
125
|
-
proofId: unit['unit'],
|
|
126
|
-
payTo: req.payTo,
|
|
127
|
-
asset: req.asset,
|
|
128
|
-
amount: req.amount.toString(),
|
|
129
|
-
binding: req.extra.binding,
|
|
130
|
-
},
|
|
131
|
-
'payment unit signed (not broadcast)',
|
|
132
|
-
);
|
|
133
|
-
// v1: the mechanism returns the complete payload (core passes it through as-is).
|
|
134
|
-
if (x402Version === 1) {
|
|
135
|
-
return {
|
|
136
|
-
x402Version: 1,
|
|
137
|
-
scheme: EXACT_SCHEME,
|
|
138
|
-
network: requirements.network,
|
|
139
|
-
payload: { unit },
|
|
140
|
-
} as unknown as PaymentPayloadResult;
|
|
141
|
-
}
|
|
142
|
-
return { x402Version, payload: { unit } };
|
|
163
|
+
return unit;
|
|
143
164
|
}
|
|
144
165
|
}
|
package/src/exact/facilitator.ts
CHANGED
|
@@ -90,6 +90,8 @@ export class ExactObyteFacilitator implements SchemeNetworkFacilitator {
|
|
|
90
90
|
private readonly now: () => number;
|
|
91
91
|
/** Serializes settle/resume per settlement key inside this process. */
|
|
92
92
|
private readonly inFlight = new Map<string, Promise<SettleResponse>>();
|
|
93
|
+
/** Known plain (non-AA) recipients; an address can never turn into an AA. */
|
|
94
|
+
private readonly plainRecipients = new Set<string>();
|
|
93
95
|
|
|
94
96
|
constructor(private readonly options: ExactObyteFacilitatorOptions) {
|
|
95
97
|
this.log = moduleLogger(options.logger, 'x402.exact', { scheme: EXACT_SCHEME });
|
|
@@ -264,7 +266,6 @@ export class ExactObyteFacilitator implements SchemeNetworkFacilitator {
|
|
|
264
266
|
now: this.now,
|
|
265
267
|
},
|
|
266
268
|
client,
|
|
267
|
-
req,
|
|
268
269
|
facts,
|
|
269
270
|
unit as Record<string, unknown>,
|
|
270
271
|
);
|
|
@@ -272,9 +273,6 @@ export class ExactObyteFacilitator implements SchemeNetworkFacilitator {
|
|
|
272
273
|
return { ok: true, value: { facts, req } };
|
|
273
274
|
}
|
|
274
275
|
|
|
275
|
-
/** Known plain (non-AA) recipients; an address can never turn into an AA. */
|
|
276
|
-
private readonly plainRecipients = new Set<string>();
|
|
277
|
-
|
|
278
276
|
private async recipientIsNotAa(client: obyte.Client, payTo: string): Promise<Check<null>> {
|
|
279
277
|
if (this.plainRecipients.has(payTo)) return { ok: true, value: null };
|
|
280
278
|
const res = await hubCall(() => client.api.getDefinition(payTo), this.hubTimeoutMs);
|
|
@@ -3,7 +3,6 @@ import type { ExactUnitFacts } from '@obbyx/obyte';
|
|
|
3
3
|
import { hubCall } from '../hub/hub-call.js';
|
|
4
4
|
import { type LedgerUnit, parseHistory, parseLedgerUnit } from '../hub/schemas.js';
|
|
5
5
|
import { type Check, EXACT_SETTLE_REASONS, fail } from './reasons.js';
|
|
6
|
-
import type { ParsedExactRequirements } from './requirements.js';
|
|
7
6
|
|
|
8
7
|
export interface LedgerCheckContext {
|
|
9
8
|
readonly hubTimeoutMs: number;
|
|
@@ -18,12 +17,25 @@ export interface LedgerCheckContext {
|
|
|
18
17
|
export async function checkLedger(
|
|
19
18
|
ctx: LedgerCheckContext,
|
|
20
19
|
client: obyte.Client,
|
|
21
|
-
req: ParsedExactRequirements,
|
|
22
20
|
facts: ExactUnitFacts,
|
|
23
21
|
unit: Record<string, unknown>,
|
|
24
22
|
): Promise<Check<null>> {
|
|
25
|
-
const
|
|
26
|
-
|
|
23
|
+
const recent = await lastBallIsRecent(ctx, client, facts);
|
|
24
|
+
if (!recent.ok) return recent;
|
|
25
|
+
const totals = await inputTotals(ctx, client, facts);
|
|
26
|
+
if (!totals.ok) return totals;
|
|
27
|
+
const balanced = inputsBalance(totals.value, facts, unit);
|
|
28
|
+
if (!balanced.ok) return balanced;
|
|
29
|
+
return inputsUnspent(ctx, client, facts);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The unit's last ball exists and is not older than the configured limit. */
|
|
33
|
+
async function lastBallIsRecent(
|
|
34
|
+
ctx: LedgerCheckContext,
|
|
35
|
+
client: obyte.Client,
|
|
36
|
+
facts: ExactUnitFacts,
|
|
37
|
+
): Promise<Check<null>> {
|
|
38
|
+
const lastBall = await hubCall(() => client.api.getJoint(facts.lastBallUnit), ctx.hubTimeoutMs);
|
|
27
39
|
if (lastBall.kind !== 'ok')
|
|
28
40
|
return fail(EXACT_SETTLE_REASONS.ledger, `last ball: ${lastBall.error}`);
|
|
29
41
|
const lastBallTs = lastBall.value.joint?.unit.timestamp;
|
|
@@ -36,14 +48,21 @@ export async function checkLedger(
|
|
|
36
48
|
`Last ball is ${lastBallAge} s old; the limit is ${ctx.maxLastBallAgeSeconds} s (MAX_LAST_BALL_AGE_SECONDS)`,
|
|
37
49
|
);
|
|
38
50
|
}
|
|
51
|
+
return { ok: true, value: null };
|
|
52
|
+
}
|
|
39
53
|
|
|
40
|
-
|
|
54
|
+
/** Each input refers to an existing output of the payer in its asset; sums per asset. */
|
|
55
|
+
async function inputTotals(
|
|
56
|
+
ctx: LedgerCheckContext,
|
|
57
|
+
client: obyte.Client,
|
|
58
|
+
facts: ExactUnitFacts,
|
|
59
|
+
): Promise<Check<Map<string, bigint>>> {
|
|
41
60
|
const totals = new Map<string, bigint>();
|
|
42
61
|
const sources = new Map<string, LedgerUnit>();
|
|
43
62
|
for (const input of facts.inputs) {
|
|
44
63
|
let source = sources.get(input.unit);
|
|
45
64
|
if (source === undefined) {
|
|
46
|
-
const res = await hubCall(() => client.api.getJoint(input.unit),
|
|
65
|
+
const res = await hubCall(() => client.api.getJoint(input.unit), ctx.hubTimeoutMs);
|
|
47
66
|
if (res.kind !== 'ok') return fail(EXACT_SETTLE_REASONS.ledger, `input source: ${res.error}`);
|
|
48
67
|
if (res.value.joint === undefined)
|
|
49
68
|
return fail(EXACT_SETTLE_REASONS.inputs, 'Input source unit not found');
|
|
@@ -71,6 +90,15 @@ export async function checkLedger(
|
|
|
71
90
|
}
|
|
72
91
|
totals.set(asset, (totals.get(asset) ?? 0n) + BigInt(output.amount));
|
|
73
92
|
}
|
|
93
|
+
return { ok: true, value: totals };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Inputs equal outputs per asset; bytes also cover every fee of the unit. */
|
|
97
|
+
function inputsBalance(
|
|
98
|
+
totals: ReadonlyMap<string, bigint>,
|
|
99
|
+
facts: ExactUnitFacts,
|
|
100
|
+
unit: Record<string, unknown>,
|
|
101
|
+
): Check<null> {
|
|
74
102
|
const fees =
|
|
75
103
|
BigInt(facts.commissions.headers + facts.commissions.payload) +
|
|
76
104
|
BigInt((unit['tps_fee'] as number | undefined) ?? 0) +
|
|
@@ -81,9 +109,18 @@ export async function checkLedger(
|
|
|
81
109
|
return fail(EXACT_SETTLE_REASONS.inputs, `Inputs and outputs do not balance in ${asset}`);
|
|
82
110
|
}
|
|
83
111
|
}
|
|
112
|
+
return { ok: true, value: null };
|
|
113
|
+
}
|
|
84
114
|
|
|
85
|
-
|
|
86
|
-
|
|
115
|
+
/**
|
|
116
|
+
* No other unit of the payer spends these inputs (best effort until
|
|
117
|
+
* stability; the network decides double spends definitively).
|
|
118
|
+
*/
|
|
119
|
+
async function inputsUnspent(
|
|
120
|
+
ctx: LedgerCheckContext,
|
|
121
|
+
client: obyte.Client,
|
|
122
|
+
facts: ExactUnitFacts,
|
|
123
|
+
): Promise<Check<null>> {
|
|
87
124
|
const history = await hubCall(
|
|
88
125
|
() =>
|
|
89
126
|
client.api
|
|
@@ -91,7 +128,7 @@ export async function checkLedger(
|
|
|
91
128
|
.then((witnesses) =>
|
|
92
129
|
client.api.getHistory({ witnesses, addresses: [facts.payer] } as never),
|
|
93
130
|
),
|
|
94
|
-
|
|
131
|
+
ctx.hubTimeoutMs,
|
|
95
132
|
);
|
|
96
133
|
if (history.kind !== 'ok') return fail(EXACT_SETTLE_REASONS.ledger, `history: ${history.error}`);
|
|
97
134
|
const parsedHistory = parseHistory(history.value);
|
package/src/hub/schemas.ts
CHANGED
|
@@ -87,3 +87,32 @@ export function parseStateVars(raw: unknown): Record<string, unknown> | undefine
|
|
|
87
87
|
const parsed = z.record(z.string(), z.unknown()).safeParse(raw);
|
|
88
88
|
return parsed.success ? parsed.data : undefined;
|
|
89
89
|
}
|
|
90
|
+
|
|
91
|
+
/** Balance of one asset: confirmed (stable) and not yet confirmed (pending), atomic units. */
|
|
92
|
+
export interface AssetBalance {
|
|
93
|
+
readonly stable: bigint;
|
|
94
|
+
readonly pending: bigint;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const balancesSchema = z.record(
|
|
98
|
+
z.string(),
|
|
99
|
+
z.record(z.string(), z.object({ stable: safeAmount, pending: safeAmount }).loose()),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* `light/get_balances` for one address: balances by asset ('base' = bytes);
|
|
104
|
+
* an address without funds has none. Undefined when the answer is malformed.
|
|
105
|
+
*/
|
|
106
|
+
export function parseBalances(
|
|
107
|
+
raw: unknown,
|
|
108
|
+
address: string,
|
|
109
|
+
): Record<string, AssetBalance> | undefined {
|
|
110
|
+
const parsed = balancesSchema.safeParse(raw);
|
|
111
|
+
if (!parsed.success) return undefined;
|
|
112
|
+
return Object.fromEntries(
|
|
113
|
+
Object.entries(parsed.data[address] ?? {}).map(([asset, b]) => [
|
|
114
|
+
asset,
|
|
115
|
+
{ stable: BigInt(b.stable), pending: BigInt(b.pending) },
|
|
116
|
+
]),
|
|
117
|
+
);
|
|
118
|
+
}
|