@loopprotocol/sdk-byoaa 0.1.0-alpha.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/CHANGELOG.md +54 -0
- package/LICENSE +21 -0
- package/README.md +113 -0
- package/dist/index.d.mts +1046 -0
- package/dist/index.d.ts +1046 -0
- package/dist/index.js +1127 -0
- package/dist/index.mjs +1068 -0
- package/package.json +65 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
AgentsNamespace: () => AgentsNamespace,
|
|
24
|
+
AttestedReceiptSubmitter: () => AttestedReceiptSubmitter,
|
|
25
|
+
AttestedReceiptVerifier: () => AttestedReceiptVerifier,
|
|
26
|
+
AuditNamespace: () => AuditNamespace,
|
|
27
|
+
DEVNET_PROGRAM_IDS: () => DEVNET_PROGRAM_IDS,
|
|
28
|
+
DecisionsNamespace: () => DecisionsNamespace,
|
|
29
|
+
LoopByoaaApiKeyError: () => LoopByoaaApiKeyError,
|
|
30
|
+
LoopByoaaClient: () => LoopByoaaClient,
|
|
31
|
+
LoopByoaaError: () => LoopByoaaError,
|
|
32
|
+
LoopByoaaNetworkError: () => LoopByoaaNetworkError,
|
|
33
|
+
LoopByoaaPermissionError: () => LoopByoaaPermissionError,
|
|
34
|
+
LoopByoaaQuotaError: () => LoopByoaaQuotaError,
|
|
35
|
+
LoopByoaaRateLimitError: () => LoopByoaaRateLimitError,
|
|
36
|
+
LoopByoaaServerError: () => LoopByoaaServerError,
|
|
37
|
+
LoopByoaaValidationError: () => LoopByoaaValidationError,
|
|
38
|
+
MAINNET_PROGRAM_IDS: () => MAINNET_PROGRAM_IDS,
|
|
39
|
+
MAX_MERCHANT_NAME_RAW_LEN: () => MAX_MERCHANT_NAME_RAW_LEN,
|
|
40
|
+
MAX_RECEIPT_AGE_SECONDS: () => MAX_RECEIPT_AGE_SECONDS,
|
|
41
|
+
MAX_RECEIPT_CENTS: () => MAX_RECEIPT_CENTS,
|
|
42
|
+
PermissionsNamespace: () => PermissionsNamespace,
|
|
43
|
+
ReceiptNotAuditedError: () => ReceiptNotAuditedError,
|
|
44
|
+
ReceiptNotFoundError: () => ReceiptNotFoundError,
|
|
45
|
+
SESSION_INSTRUCTION_INDEX_SUBMIT_ATTESTED_RECEIPT: () => SESSION_INSTRUCTION_INDEX_SUBMIT_ATTESTED_RECEIPT,
|
|
46
|
+
UnsupportedProofTypeError: () => UnsupportedProofTypeError,
|
|
47
|
+
decodeBankAttestedReceipt: () => decodeBankAttestedReceipt,
|
|
48
|
+
decodeRegistryPcr0Set: () => decodeRegistryPcr0Set,
|
|
49
|
+
defaultRpcUrl: () => defaultRpcUrl,
|
|
50
|
+
deriveAgentSessionPda: () => deriveAgentSessionPda,
|
|
51
|
+
deriveBankAttestedReceiptPda: () => deriveBankAttestedReceiptPda,
|
|
52
|
+
deriveBankTxnId: () => deriveBankTxnId,
|
|
53
|
+
deriveShoppingStatePda: () => deriveShoppingStatePda,
|
|
54
|
+
encodeAttestedReceiptArgs: () => encodeAttestedReceiptArgs,
|
|
55
|
+
encodeSubmitAttestedReceiptIxData: () => encodeSubmitAttestedReceiptIxData,
|
|
56
|
+
getProgramIds: () => getProgramIds,
|
|
57
|
+
instructionDiscriminator: () => instructionDiscriminator,
|
|
58
|
+
isMainnet: () => isMainnet,
|
|
59
|
+
normalizeMerchantName: () => normalizeMerchantName,
|
|
60
|
+
parseLoopByoaaSdkKey: () => parseLoopByoaaSdkKey,
|
|
61
|
+
resolveConfig: () => resolveConfig,
|
|
62
|
+
validateReceipt: () => validateReceipt,
|
|
63
|
+
verifyAttestedReceiptProof: () => verifyAttestedReceiptProof
|
|
64
|
+
});
|
|
65
|
+
module.exports = __toCommonJS(index_exports);
|
|
66
|
+
|
|
67
|
+
// src/types.ts
|
|
68
|
+
var import_node_crypto = require("crypto");
|
|
69
|
+
var MAX_RECEIPT_CENTS = 10000000000n;
|
|
70
|
+
var MAX_MERCHANT_NAME_RAW_LEN = 64;
|
|
71
|
+
var MAX_RECEIPT_AGE_SECONDS = 365n * 24n * 60n * 60n;
|
|
72
|
+
function normalizeMerchantName(raw) {
|
|
73
|
+
const ascii = raw.replace(/[^\x20-\x7E]/g, "");
|
|
74
|
+
return ascii.trim().replace(/\s+/g, " ").toUpperCase();
|
|
75
|
+
}
|
|
76
|
+
function deriveBankTxnId(input) {
|
|
77
|
+
const normalized = normalizeMerchantName(input.merchantNameRaw);
|
|
78
|
+
const payload = [
|
|
79
|
+
normalized,
|
|
80
|
+
input.amountCents.toString(),
|
|
81
|
+
input.postedAt.toString(),
|
|
82
|
+
input.accountLast4.toString()
|
|
83
|
+
].join("|");
|
|
84
|
+
const hash = (0, import_node_crypto.createHash)("sha256").update(payload, "utf8").digest();
|
|
85
|
+
return new Uint8Array(hash);
|
|
86
|
+
}
|
|
87
|
+
function validateReceipt(receipt, nowSeconds = BigInt(Math.floor(Date.now() / 1e3))) {
|
|
88
|
+
if (receipt.bankTxnId.length !== 32) {
|
|
89
|
+
return { ok: false, error: "bank_txn_id_wrong_length" };
|
|
90
|
+
}
|
|
91
|
+
if (receipt.merchantNameRaw.length === 0) {
|
|
92
|
+
return { ok: false, error: "merchant_name_empty" };
|
|
93
|
+
}
|
|
94
|
+
if (receipt.merchantNameRaw.length > MAX_MERCHANT_NAME_RAW_LEN) {
|
|
95
|
+
return { ok: false, error: "merchant_name_too_long" };
|
|
96
|
+
}
|
|
97
|
+
if (!/^[\x20-\x7E]+$/.test(receipt.merchantNameRaw)) {
|
|
98
|
+
return { ok: false, error: "merchant_name_not_ascii" };
|
|
99
|
+
}
|
|
100
|
+
if (receipt.amountCents <= 0n) {
|
|
101
|
+
return { ok: false, error: "amount_zero" };
|
|
102
|
+
}
|
|
103
|
+
if (receipt.amountCents > MAX_RECEIPT_CENTS) {
|
|
104
|
+
return { ok: false, error: "amount_too_large" };
|
|
105
|
+
}
|
|
106
|
+
if (receipt.postedAt > nowSeconds) {
|
|
107
|
+
return { ok: false, error: "posted_at_in_future" };
|
|
108
|
+
}
|
|
109
|
+
if (nowSeconds - receipt.postedAt > MAX_RECEIPT_AGE_SECONDS) {
|
|
110
|
+
return { ok: false, error: "posted_at_too_old" };
|
|
111
|
+
}
|
|
112
|
+
return { ok: true };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/config.ts
|
|
116
|
+
var import_web3 = require("@solana/web3.js");
|
|
117
|
+
var DEVNET_PROGRAM_IDS = {
|
|
118
|
+
SHOPPING: new import_web3.PublicKey("FSqRkH7nkGHP3VpHwFE667PVAfLKfSGaPMgTrXpJZJoJ"),
|
|
119
|
+
VAULT: new import_web3.PublicKey("9gKRCrUpHv9CRHwYMmm2zP5bN1bUKgyi7BxuS5jifX4x")
|
|
120
|
+
};
|
|
121
|
+
var MAINNET_PROGRAM_IDS = {
|
|
122
|
+
// TODO(post-Bundle-2-deploy): replace with the live mainnet id.
|
|
123
|
+
SHOPPING: new import_web3.PublicKey("11111111111111111111111111111111"),
|
|
124
|
+
// Live mainnet vault id (already deployed). Fixed.
|
|
125
|
+
VAULT: new import_web3.PublicKey("J8HhLeRv5iQaSyYQBXJoDwDKbw4V8uA84WN93YrVSWQT")
|
|
126
|
+
};
|
|
127
|
+
function resolveConfig(input) {
|
|
128
|
+
const cfg = typeof input === "string" ? { network: input } : input;
|
|
129
|
+
const programIds = getProgramIds(cfg.network);
|
|
130
|
+
const rpcUrl = cfg.rpcUrl ?? defaultRpcUrl(cfg.network);
|
|
131
|
+
const commitment = cfg.commitment ?? "confirmed";
|
|
132
|
+
const connection = new import_web3.Connection(
|
|
133
|
+
rpcUrl,
|
|
134
|
+
cfg.connectionConfig ?? { commitment }
|
|
135
|
+
);
|
|
136
|
+
return {
|
|
137
|
+
network: cfg.network,
|
|
138
|
+
connection,
|
|
139
|
+
shoppingProgramId: cfg.shoppingProgramId ?? programIds.SHOPPING,
|
|
140
|
+
vaultProgramId: cfg.vaultProgramId ?? programIds.VAULT
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function getProgramIds(network) {
|
|
144
|
+
switch (network) {
|
|
145
|
+
case "devnet":
|
|
146
|
+
case "localnet":
|
|
147
|
+
return DEVNET_PROGRAM_IDS;
|
|
148
|
+
case "mainnet-beta":
|
|
149
|
+
return MAINNET_PROGRAM_IDS;
|
|
150
|
+
default: {
|
|
151
|
+
const _exhaustive = network;
|
|
152
|
+
throw new Error(`Unknown network: ${_exhaustive}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function defaultRpcUrl(network) {
|
|
157
|
+
if (network === "localnet") return "http://127.0.0.1:8899";
|
|
158
|
+
return (0, import_web3.clusterApiUrl)(network);
|
|
159
|
+
}
|
|
160
|
+
function isMainnet(network) {
|
|
161
|
+
return network === "mainnet-beta";
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/pda.ts
|
|
165
|
+
var import_web32 = require("@solana/web3.js");
|
|
166
|
+
function deriveBankAttestedReceiptPda(args) {
|
|
167
|
+
if (args.bankTxnId.length !== 32) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
`bankTxnId must be exactly 32 bytes; got ${args.bankTxnId.length}`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
const [pda, bump] = import_web32.PublicKey.findProgramAddressSync(
|
|
173
|
+
[
|
|
174
|
+
Buffer.from("attested_receipt"),
|
|
175
|
+
args.vault.toBuffer(),
|
|
176
|
+
Buffer.from(args.bankTxnId)
|
|
177
|
+
],
|
|
178
|
+
args.shoppingProgramId
|
|
179
|
+
);
|
|
180
|
+
return { pda, bump };
|
|
181
|
+
}
|
|
182
|
+
function deriveShoppingStatePda(args) {
|
|
183
|
+
const [pda, bump] = import_web32.PublicKey.findProgramAddressSync(
|
|
184
|
+
[Buffer.from("state")],
|
|
185
|
+
args.shoppingProgramId
|
|
186
|
+
);
|
|
187
|
+
return { pda, bump };
|
|
188
|
+
}
|
|
189
|
+
function deriveAgentSessionPda(args) {
|
|
190
|
+
const [pda, bump] = import_web32.PublicKey.findProgramAddressSync(
|
|
191
|
+
[
|
|
192
|
+
Buffer.from("agent_session"),
|
|
193
|
+
args.vault.toBuffer(),
|
|
194
|
+
args.sessionPubkey.toBuffer()
|
|
195
|
+
],
|
|
196
|
+
args.vaultProgramId
|
|
197
|
+
);
|
|
198
|
+
return { pda, bump };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/anchor-codec.ts
|
|
202
|
+
var import_node_crypto2 = require("crypto");
|
|
203
|
+
function instructionDiscriminator(snakeName) {
|
|
204
|
+
const h = (0, import_node_crypto2.createHash)("sha256").update(`global:${snakeName}`).digest();
|
|
205
|
+
return new Uint8Array(h.subarray(0, 8));
|
|
206
|
+
}
|
|
207
|
+
function encodeAttestedReceiptArgs(args) {
|
|
208
|
+
if (args.bankTxnId.length !== 32) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
`bankTxnId must be exactly 32 bytes; got ${args.bankTxnId.length}`
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
const nameBytes = new TextEncoder().encode(args.merchantNameRaw);
|
|
214
|
+
const buf = new Uint8Array(32 + 4 + nameBytes.length + 2 + 8 + 8 + 2);
|
|
215
|
+
const view = new DataView(buf.buffer);
|
|
216
|
+
let off = 0;
|
|
217
|
+
buf.set(args.bankTxnId, off);
|
|
218
|
+
off += 32;
|
|
219
|
+
view.setUint32(
|
|
220
|
+
off,
|
|
221
|
+
nameBytes.length,
|
|
222
|
+
true
|
|
223
|
+
/* little-endian */
|
|
224
|
+
);
|
|
225
|
+
off += 4;
|
|
226
|
+
buf.set(nameBytes, off);
|
|
227
|
+
off += nameBytes.length;
|
|
228
|
+
view.setUint16(off, args.mcc & 65535, true);
|
|
229
|
+
off += 2;
|
|
230
|
+
setU64LE(view, off, args.amountCents);
|
|
231
|
+
off += 8;
|
|
232
|
+
setI64LE(view, off, args.postedAt);
|
|
233
|
+
off += 8;
|
|
234
|
+
view.setUint16(off, args.accountLast4 & 65535, true);
|
|
235
|
+
off += 2;
|
|
236
|
+
return buf;
|
|
237
|
+
}
|
|
238
|
+
function encodeSubmitAttestedReceiptIxData(args) {
|
|
239
|
+
const disc = instructionDiscriminator("submit_attested_receipt");
|
|
240
|
+
const argBytes = encodeAttestedReceiptArgs(args);
|
|
241
|
+
const out = new Uint8Array(disc.length + argBytes.length);
|
|
242
|
+
out.set(disc, 0);
|
|
243
|
+
out.set(argBytes, disc.length);
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
246
|
+
function setU64LE(view, offset, value) {
|
|
247
|
+
if (value < 0n || value > 0xffffffffffffffffn) {
|
|
248
|
+
throw new RangeError(`u64 out of range: ${value}`);
|
|
249
|
+
}
|
|
250
|
+
view.setBigUint64(offset, value, true);
|
|
251
|
+
}
|
|
252
|
+
function setI64LE(view, offset, value) {
|
|
253
|
+
const min = -(1n << 63n);
|
|
254
|
+
const max = (1n << 63n) - 1n;
|
|
255
|
+
if (value < min || value > max) {
|
|
256
|
+
throw new RangeError(`i64 out of range: ${value}`);
|
|
257
|
+
}
|
|
258
|
+
view.setBigInt64(offset, value, true);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// src/enclave.ts
|
|
262
|
+
var import_web33 = require("@solana/web3.js");
|
|
263
|
+
var SESSION_INSTRUCTION_INDEX_SUBMIT_ATTESTED_RECEIPT = 5;
|
|
264
|
+
var AttestedReceiptSubmitter = class {
|
|
265
|
+
constructor(opts) {
|
|
266
|
+
/** Cached so we don't round-trip the enclave on every submit. */
|
|
267
|
+
this.cachedSessionPubkey = null;
|
|
268
|
+
this.resolvedConfig = resolveConfig(opts.network ?? "devnet");
|
|
269
|
+
this.enclaveSigner = opts.enclaveSigner;
|
|
270
|
+
this.vault = opts.vault;
|
|
271
|
+
this.sessionRegistrationPda = deriveAgentSessionPda({
|
|
272
|
+
vaultProgramId: this.resolvedConfig.vaultProgramId,
|
|
273
|
+
vault: opts.vault,
|
|
274
|
+
sessionPubkey: opts.sessionPubkey
|
|
275
|
+
}).pda;
|
|
276
|
+
this.commitment = opts.commitment ?? "confirmed";
|
|
277
|
+
}
|
|
278
|
+
/** Network selector this submitter was constructed against. */
|
|
279
|
+
get network() {
|
|
280
|
+
return this.resolvedConfig.network;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Resolve the receipt PDA for a given txn id without submitting.
|
|
284
|
+
* Useful for off-chain state checks ("have we already submitted X?")
|
|
285
|
+
* before paying RPC fees.
|
|
286
|
+
*/
|
|
287
|
+
deriveReceiptPda(bankTxnId) {
|
|
288
|
+
return deriveBankAttestedReceiptPda({
|
|
289
|
+
shoppingProgramId: this.resolvedConfig.shoppingProgramId,
|
|
290
|
+
vault: this.vault,
|
|
291
|
+
bankTxnId
|
|
292
|
+
}).pda;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Sign + submit one receipt. Returns the on-chain signature + the
|
|
296
|
+
* receipt's PDA on success. Failures are returned as discriminated
|
|
297
|
+
* unions rather than thrown — batch consumers want fail-isolation.
|
|
298
|
+
*/
|
|
299
|
+
async submit(receipt) {
|
|
300
|
+
const validation = validateReceipt(receipt);
|
|
301
|
+
if (!validation.ok) {
|
|
302
|
+
return failV(validation.error);
|
|
303
|
+
}
|
|
304
|
+
let sessionSigner;
|
|
305
|
+
try {
|
|
306
|
+
sessionSigner = await this.getSessionPubkey();
|
|
307
|
+
} catch (err) {
|
|
308
|
+
return fail("enclave_sign_failed", errorMessage(err));
|
|
309
|
+
}
|
|
310
|
+
const receiptPda = this.deriveReceiptPda(receipt.bankTxnId);
|
|
311
|
+
const ix = this.buildSubmitIx({
|
|
312
|
+
receipt,
|
|
313
|
+
receiptPda,
|
|
314
|
+
sessionSigner
|
|
315
|
+
});
|
|
316
|
+
let blockhashCtx;
|
|
317
|
+
try {
|
|
318
|
+
blockhashCtx = await this.resolvedConfig.connection.getLatestBlockhash(
|
|
319
|
+
this.commitment
|
|
320
|
+
);
|
|
321
|
+
} catch (err) {
|
|
322
|
+
return fail("blockhash_fetch_failed", errorMessage(err));
|
|
323
|
+
}
|
|
324
|
+
const tx = new import_web33.Transaction({
|
|
325
|
+
feePayer: sessionSigner,
|
|
326
|
+
blockhash: blockhashCtx.blockhash,
|
|
327
|
+
lastValidBlockHeight: blockhashCtx.lastValidBlockHeight
|
|
328
|
+
}).add(ix);
|
|
329
|
+
const message = tx.serializeMessage();
|
|
330
|
+
let signature;
|
|
331
|
+
try {
|
|
332
|
+
signature = await this.enclaveSigner.signInstruction({
|
|
333
|
+
instructionIndex: SESSION_INSTRUCTION_INDEX_SUBMIT_ATTESTED_RECEIPT,
|
|
334
|
+
message,
|
|
335
|
+
policyInputs: {
|
|
336
|
+
amountCents: receipt.amountCents,
|
|
337
|
+
merchantNameRaw: receipt.merchantNameRaw,
|
|
338
|
+
postedAt: receipt.postedAt,
|
|
339
|
+
bankTxnId: receipt.bankTxnId
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
} catch (err) {
|
|
343
|
+
return fail("enclave_sign_failed", errorMessage(err));
|
|
344
|
+
}
|
|
345
|
+
if (signature.length !== 64) {
|
|
346
|
+
return fail(
|
|
347
|
+
"enclave_sign_failed",
|
|
348
|
+
`expected 64-byte ed25519 signature; got ${signature.length}`
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
tx.addSignature(sessionSigner, Buffer.from(signature));
|
|
352
|
+
if (!tx.verifySignatures()) {
|
|
353
|
+
return fail(
|
|
354
|
+
"session_pubkey_mismatch",
|
|
355
|
+
"enclave signature does not verify against session_signer pubkey"
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
let txSig;
|
|
359
|
+
try {
|
|
360
|
+
const sendOpts = {
|
|
361
|
+
skipPreflight: false,
|
|
362
|
+
preflightCommitment: this.commitment
|
|
363
|
+
};
|
|
364
|
+
txSig = await this.resolvedConfig.connection.sendRawTransaction(
|
|
365
|
+
tx.serialize(),
|
|
366
|
+
sendOpts
|
|
367
|
+
);
|
|
368
|
+
} catch (err) {
|
|
369
|
+
return fail("transaction_send_failed", errorMessage(err));
|
|
370
|
+
}
|
|
371
|
+
try {
|
|
372
|
+
await this.resolvedConfig.connection.confirmTransaction(
|
|
373
|
+
{
|
|
374
|
+
signature: txSig,
|
|
375
|
+
blockhash: blockhashCtx.blockhash,
|
|
376
|
+
lastValidBlockHeight: blockhashCtx.lastValidBlockHeight
|
|
377
|
+
},
|
|
378
|
+
this.commitment
|
|
379
|
+
);
|
|
380
|
+
} catch (err) {
|
|
381
|
+
return fail("transaction_confirm_failed", errorMessage(err));
|
|
382
|
+
}
|
|
383
|
+
return { ok: true, signature: txSig, receiptPda };
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Submit N receipts with bounded concurrency + optional inter-request
|
|
387
|
+
* spacing. Fail-isolated: one bad receipt does not poison the rest.
|
|
388
|
+
*
|
|
389
|
+
* Order of `results` matches the order of `receipts`.
|
|
390
|
+
*/
|
|
391
|
+
async submitBatch(receipts, opts = {}) {
|
|
392
|
+
const concurrency = Math.max(1, opts.concurrency ?? 4);
|
|
393
|
+
const rateLimitMs = Math.max(0, opts.rateLimitMs ?? 0);
|
|
394
|
+
const total = receipts.length;
|
|
395
|
+
const results = new Array(total);
|
|
396
|
+
let nextIndex = 0;
|
|
397
|
+
const worker = async () => {
|
|
398
|
+
while (true) {
|
|
399
|
+
const i = nextIndex++;
|
|
400
|
+
if (i >= total) return;
|
|
401
|
+
if (rateLimitMs > 0 && i > 0) {
|
|
402
|
+
await sleep(rateLimitMs);
|
|
403
|
+
}
|
|
404
|
+
const r = await this.submit(receipts[i]);
|
|
405
|
+
results[i] = r;
|
|
406
|
+
opts.onProgress?.({ index: i, total, result: r });
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
await Promise.all(
|
|
410
|
+
Array.from({ length: Math.min(concurrency, total) }, () => worker())
|
|
411
|
+
);
|
|
412
|
+
return results;
|
|
413
|
+
}
|
|
414
|
+
// ────────────────────────────────────────────────────────────────────
|
|
415
|
+
// Internal
|
|
416
|
+
// ────────────────────────────────────────────────────────────────────
|
|
417
|
+
async getSessionPubkey() {
|
|
418
|
+
if (this.cachedSessionPubkey) return this.cachedSessionPubkey;
|
|
419
|
+
const pk = await this.enclaveSigner.getSessionPubkey();
|
|
420
|
+
this.cachedSessionPubkey = pk;
|
|
421
|
+
return pk;
|
|
422
|
+
}
|
|
423
|
+
buildSubmitIx(args) {
|
|
424
|
+
const data = encodeSubmitAttestedReceiptIxData({
|
|
425
|
+
bankTxnId: args.receipt.bankTxnId,
|
|
426
|
+
merchantNameRaw: args.receipt.merchantNameRaw,
|
|
427
|
+
mcc: args.receipt.mcc,
|
|
428
|
+
amountCents: args.receipt.amountCents,
|
|
429
|
+
postedAt: args.receipt.postedAt,
|
|
430
|
+
accountLast4: args.receipt.accountLast4
|
|
431
|
+
});
|
|
432
|
+
const statePda = deriveShoppingStatePda({
|
|
433
|
+
shoppingProgramId: this.resolvedConfig.shoppingProgramId
|
|
434
|
+
}).pda;
|
|
435
|
+
return new import_web33.TransactionInstruction({
|
|
436
|
+
programId: this.resolvedConfig.shoppingProgramId,
|
|
437
|
+
keys: [
|
|
438
|
+
{ pubkey: args.sessionSigner, isSigner: true, isWritable: true },
|
|
439
|
+
{ pubkey: statePda, isSigner: false, isWritable: false },
|
|
440
|
+
{ pubkey: this.vault, isSigner: false, isWritable: false },
|
|
441
|
+
{ pubkey: this.sessionRegistrationPda, isSigner: false, isWritable: false },
|
|
442
|
+
{ pubkey: args.receiptPda, isSigner: false, isWritable: true },
|
|
443
|
+
{ pubkey: import_web33.SystemProgram.programId, isSigner: false, isWritable: false }
|
|
444
|
+
],
|
|
445
|
+
data: Buffer.from(data)
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
function fail(error, detail) {
|
|
450
|
+
return detail !== void 0 ? { ok: false, error, detail } : { ok: false, error };
|
|
451
|
+
}
|
|
452
|
+
function failV(error) {
|
|
453
|
+
return { ok: false, error: "validation_failed", detail: error };
|
|
454
|
+
}
|
|
455
|
+
function errorMessage(err) {
|
|
456
|
+
if (err instanceof Error) return err.message;
|
|
457
|
+
return String(err);
|
|
458
|
+
}
|
|
459
|
+
function sleep(ms) {
|
|
460
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// src/verifier.ts
|
|
464
|
+
var import_web34 = require("@solana/web3.js");
|
|
465
|
+
var import_node_crypto3 = require("crypto");
|
|
466
|
+
var ReceiptNotAuditedError = class extends Error {
|
|
467
|
+
constructor(receipt) {
|
|
468
|
+
super(
|
|
469
|
+
`Receipt ${receipt.pda.toBase58()} produced by an unaudited enclave image (pcr0 not in EnclaveImageRegistry)`
|
|
470
|
+
);
|
|
471
|
+
this.receipt = receipt;
|
|
472
|
+
this.name = "ReceiptNotAuditedError";
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
var ReceiptNotFoundError = class extends Error {
|
|
476
|
+
constructor(pda) {
|
|
477
|
+
super(`No BankAttestedReceipt account at ${pda.toBase58()}`);
|
|
478
|
+
this.pda = pda;
|
|
479
|
+
this.name = "ReceiptNotFoundError";
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
var UnsupportedProofTypeError = class extends Error {
|
|
483
|
+
constructor(proofType) {
|
|
484
|
+
super(`Unsupported receipt proof type: ${proofType}`);
|
|
485
|
+
this.proofType = proofType;
|
|
486
|
+
this.name = "UnsupportedProofTypeError";
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
function verifyAttestedReceiptProof(receipt) {
|
|
490
|
+
switch (receipt.proof.proof_type) {
|
|
491
|
+
case "cose_sign1_x509":
|
|
492
|
+
verifyCoseSign1X509ProofShape(receipt);
|
|
493
|
+
return { ok: true, proof_type: "cose_sign1_x509" };
|
|
494
|
+
case "risc0_v1":
|
|
495
|
+
case "groth16_alt_bn128":
|
|
496
|
+
throw new UnsupportedProofTypeError(receipt.proof.proof_type);
|
|
497
|
+
default: {
|
|
498
|
+
const unreachable = receipt.proof;
|
|
499
|
+
throw new UnsupportedProofTypeError(
|
|
500
|
+
unreachable.proof_type
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
function verifyCoseSign1X509ProofShape(receipt) {
|
|
506
|
+
if (receipt.proof.proof_type !== "cose_sign1_x509") return;
|
|
507
|
+
if (!(receipt.proof.signature instanceof Uint8Array)) {
|
|
508
|
+
throw new Error("cose_sign1_x509 signature must be bytes");
|
|
509
|
+
}
|
|
510
|
+
if (!Array.isArray(receipt.proof.cert_chain)) {
|
|
511
|
+
throw new Error("cose_sign1_x509 cert_chain must be an array");
|
|
512
|
+
}
|
|
513
|
+
for (const cert of receipt.proof.cert_chain) {
|
|
514
|
+
if (!(cert instanceof Uint8Array)) {
|
|
515
|
+
throw new Error("cose_sign1_x509 cert_chain entries must be bytes");
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
var BANK_ATTESTED_RECEIPT_DISC = anchorAccountDiscriminator(
|
|
520
|
+
"BankAttestedReceipt"
|
|
521
|
+
);
|
|
522
|
+
var ENCLAVE_IMAGE_REGISTRY_DISC = anchorAccountDiscriminator(
|
|
523
|
+
"EnclaveImageRegistry"
|
|
524
|
+
);
|
|
525
|
+
function anchorAccountDiscriminator(typeName) {
|
|
526
|
+
const h = (0, import_node_crypto3.createHash)("sha256").update(`account:${typeName}`).digest();
|
|
527
|
+
return new Uint8Array(h.subarray(0, 8));
|
|
528
|
+
}
|
|
529
|
+
var AttestedReceiptVerifier = class {
|
|
530
|
+
constructor(opts = {}) {
|
|
531
|
+
/** Cache of approved PCR0s. */
|
|
532
|
+
this.approvedPcr0s = null;
|
|
533
|
+
this.approvedPcr0sExpiresAt = 0;
|
|
534
|
+
this.resolvedConfig = resolveConfig(opts.network ?? "devnet");
|
|
535
|
+
this.registryCacheMs = opts.registryCacheMs ?? 6e4;
|
|
536
|
+
this.skipImageAudit = opts.skipImageAudit ?? false;
|
|
537
|
+
}
|
|
538
|
+
get network() {
|
|
539
|
+
return this.resolvedConfig.network;
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Fetch one receipt by PDA. Throws `ReceiptNotFoundError` if the
|
|
543
|
+
* account is missing or `ReceiptNotAuditedError` when
|
|
544
|
+
* `requireAuditedImage` is set and the PCR0 doesn't match the registry.
|
|
545
|
+
*/
|
|
546
|
+
async fetch(pda, opts = {}) {
|
|
547
|
+
const account = await this.resolvedConfig.connection.getAccountInfo(
|
|
548
|
+
pda,
|
|
549
|
+
this.resolvedConfig.connection.commitment ?? "confirmed"
|
|
550
|
+
);
|
|
551
|
+
if (!account) throw new ReceiptNotFoundError(pda);
|
|
552
|
+
const decoded = decodeBankAttestedReceipt(pda, account);
|
|
553
|
+
const audited = await this.auditPcr0(decoded.pcr0);
|
|
554
|
+
decoded.imageAudited = audited;
|
|
555
|
+
if (opts.requireAuditedImage && audited === false) {
|
|
556
|
+
throw new ReceiptNotAuditedError(decoded);
|
|
557
|
+
}
|
|
558
|
+
return decoded;
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Stream every receipt for a given vault. Uses `getProgramAccounts`
|
|
562
|
+
* with a memcmp filter on the `vault` field at fixed offset 8 (right
|
|
563
|
+
* after the 8-byte Anchor discriminator).
|
|
564
|
+
*
|
|
565
|
+
* Order is undefined — sort client-side if you need deterministic
|
|
566
|
+
* ordering (e.g. by `submittedAt` or `postedAt`).
|
|
567
|
+
*/
|
|
568
|
+
async *forVault(vault) {
|
|
569
|
+
const filters = [
|
|
570
|
+
// Discriminator filter — only BankAttestedReceipt accounts.
|
|
571
|
+
{
|
|
572
|
+
memcmp: {
|
|
573
|
+
offset: 0,
|
|
574
|
+
bytes: bs58Encode(BANK_ATTESTED_RECEIPT_DISC)
|
|
575
|
+
}
|
|
576
|
+
},
|
|
577
|
+
// Vault filter at offset 8 (immediately after discriminator).
|
|
578
|
+
{
|
|
579
|
+
memcmp: {
|
|
580
|
+
offset: 8,
|
|
581
|
+
bytes: bs58Encode(vault.toBytes())
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
];
|
|
585
|
+
const accounts = await this.resolvedConfig.connection.getProgramAccounts(
|
|
586
|
+
this.resolvedConfig.shoppingProgramId,
|
|
587
|
+
{ filters }
|
|
588
|
+
);
|
|
589
|
+
for (const { pubkey, account } of accounts) {
|
|
590
|
+
const decoded = decodeBankAttestedReceipt(pubkey, account);
|
|
591
|
+
decoded.imageAudited = await this.auditPcr0(decoded.pcr0);
|
|
592
|
+
yield decoded;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Stream receipts whose `merchantNameRaw` (after `normalizeMerchantName`)
|
|
597
|
+
* matches the given normalized name.
|
|
598
|
+
*
|
|
599
|
+
* **Implementation note:** `merchant_name_raw` lives at variable byte
|
|
600
|
+
* offset (it follows a 4-byte length prefix that varies per receipt),
|
|
601
|
+
* so memcmp filtering on-chain is impractical. This method therefore
|
|
602
|
+
* fetches every `BankAttestedReceipt` account from the program and
|
|
603
|
+
* filters client-side. **Use the off-chain `attested_receipt_index`
|
|
604
|
+
* Supabase mirror (loop-site-v2 PR #27) for production-scale
|
|
605
|
+
* merchant queries.** This SDK method is fine for low-volume audits
|
|
606
|
+
* + correctness testing.
|
|
607
|
+
*/
|
|
608
|
+
async *forMerchant(merchantNameNormalized) {
|
|
609
|
+
const target = normalizeMerchantName(merchantNameNormalized);
|
|
610
|
+
const filters = [
|
|
611
|
+
{
|
|
612
|
+
memcmp: {
|
|
613
|
+
offset: 0,
|
|
614
|
+
bytes: bs58Encode(BANK_ATTESTED_RECEIPT_DISC)
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
];
|
|
618
|
+
const accounts = await this.resolvedConfig.connection.getProgramAccounts(
|
|
619
|
+
this.resolvedConfig.shoppingProgramId,
|
|
620
|
+
{ filters }
|
|
621
|
+
);
|
|
622
|
+
for (const { pubkey, account } of accounts) {
|
|
623
|
+
const decoded = decodeBankAttestedReceipt(pubkey, account);
|
|
624
|
+
if (normalizeMerchantName(decoded.merchantNameRaw) !== target) continue;
|
|
625
|
+
decoded.imageAudited = await this.auditPcr0(decoded.pcr0);
|
|
626
|
+
yield decoded;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Force a refresh of the EnclaveImageRegistry cache. Useful in long-
|
|
631
|
+
* running daemons that want to pick up admin updates immediately.
|
|
632
|
+
*/
|
|
633
|
+
async refreshImageRegistry() {
|
|
634
|
+
this.approvedPcr0sExpiresAt = 0;
|
|
635
|
+
await this.loadApprovedPcr0s();
|
|
636
|
+
}
|
|
637
|
+
// ────────────────────────────────────────────────────────────────────
|
|
638
|
+
// Internal
|
|
639
|
+
// ────────────────────────────────────────────────────────────────────
|
|
640
|
+
async auditPcr0(pcr0) {
|
|
641
|
+
if (this.skipImageAudit) return null;
|
|
642
|
+
const set = await this.loadApprovedPcr0s();
|
|
643
|
+
return set.has(toHex(pcr0));
|
|
644
|
+
}
|
|
645
|
+
async loadApprovedPcr0s() {
|
|
646
|
+
if (this.approvedPcr0s && Date.now() < this.approvedPcr0sExpiresAt) {
|
|
647
|
+
return this.approvedPcr0s;
|
|
648
|
+
}
|
|
649
|
+
const [registryPda] = import_web34.PublicKey.findProgramAddressSync(
|
|
650
|
+
[Buffer.from("enclave_image_registry")],
|
|
651
|
+
this.resolvedConfig.vaultProgramId
|
|
652
|
+
);
|
|
653
|
+
const account = await this.resolvedConfig.connection.getAccountInfo(
|
|
654
|
+
registryPda
|
|
655
|
+
);
|
|
656
|
+
const set = account ? decodeRegistryPcr0Set(account) : /* @__PURE__ */ new Set();
|
|
657
|
+
this.approvedPcr0s = set;
|
|
658
|
+
this.approvedPcr0sExpiresAt = Date.now() + this.registryCacheMs;
|
|
659
|
+
return set;
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
function decodeBankAttestedReceipt(pda, account) {
|
|
663
|
+
const data = account.data;
|
|
664
|
+
if (data.length < 8) {
|
|
665
|
+
throw new Error(`Account too small to be a BankAttestedReceipt: ${data.length} bytes`);
|
|
666
|
+
}
|
|
667
|
+
if (!bytesEqual(data.subarray(0, 8), BANK_ATTESTED_RECEIPT_DISC)) {
|
|
668
|
+
throw new Error(
|
|
669
|
+
`Discriminator mismatch at ${pda.toBase58()} \u2014 not a BankAttestedReceipt`
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
let off = 8;
|
|
673
|
+
const vault = new import_web34.PublicKey(data.subarray(off, off + 32));
|
|
674
|
+
off += 32;
|
|
675
|
+
const sessionPubkey = new import_web34.PublicKey(data.subarray(off, off + 32));
|
|
676
|
+
off += 32;
|
|
677
|
+
const pcr0 = new Uint8Array(data.subarray(off, off + 48));
|
|
678
|
+
off += 48;
|
|
679
|
+
const bankTxnId = new Uint8Array(data.subarray(off, off + 32));
|
|
680
|
+
off += 32;
|
|
681
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
682
|
+
const nameLen = view.getUint32(off, true);
|
|
683
|
+
off += 4;
|
|
684
|
+
if (nameLen > 64) {
|
|
685
|
+
throw new Error(
|
|
686
|
+
`merchant_name_raw length ${nameLen} exceeds on-chain cap of 64`
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
const merchantNameRaw = new TextDecoder().decode(
|
|
690
|
+
data.subarray(off, off + nameLen)
|
|
691
|
+
);
|
|
692
|
+
off += nameLen;
|
|
693
|
+
const mcc = view.getUint16(off, true);
|
|
694
|
+
off += 2;
|
|
695
|
+
const amountCents = view.getBigUint64(off, true);
|
|
696
|
+
off += 8;
|
|
697
|
+
const postedAt = view.getBigInt64(off, true);
|
|
698
|
+
off += 8;
|
|
699
|
+
const accountLast4 = view.getUint16(off, true);
|
|
700
|
+
off += 2;
|
|
701
|
+
const submittedAt = view.getBigInt64(off, true);
|
|
702
|
+
off += 8;
|
|
703
|
+
const statusRaw = view.getUint8(off);
|
|
704
|
+
off += 1;
|
|
705
|
+
return {
|
|
706
|
+
pda,
|
|
707
|
+
vault,
|
|
708
|
+
sessionPubkey,
|
|
709
|
+
pcr0,
|
|
710
|
+
bankTxnId,
|
|
711
|
+
merchantNameRaw,
|
|
712
|
+
mcc,
|
|
713
|
+
amountCents,
|
|
714
|
+
postedAt,
|
|
715
|
+
accountLast4,
|
|
716
|
+
submittedAt,
|
|
717
|
+
status: statusFromByte(statusRaw),
|
|
718
|
+
imageAudited: null
|
|
719
|
+
// populated by the verifier
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
function statusFromByte(byte) {
|
|
723
|
+
switch (byte) {
|
|
724
|
+
case 0:
|
|
725
|
+
return "recorded";
|
|
726
|
+
case 1:
|
|
727
|
+
return "honored";
|
|
728
|
+
case 2:
|
|
729
|
+
return "invalidated";
|
|
730
|
+
default:
|
|
731
|
+
throw new Error(`Unknown receipt status byte: ${byte}`);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
function decodeRegistryPcr0Set(account) {
|
|
735
|
+
const data = account.data;
|
|
736
|
+
if (data.length < 8) return /* @__PURE__ */ new Set();
|
|
737
|
+
if (!bytesEqual(data.subarray(0, 8), ENCLAVE_IMAGE_REGISTRY_DISC)) {
|
|
738
|
+
return /* @__PURE__ */ new Set();
|
|
739
|
+
}
|
|
740
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
741
|
+
let off = 8;
|
|
742
|
+
off += 32;
|
|
743
|
+
const vecLen = view.getUint32(off, true);
|
|
744
|
+
off += 4;
|
|
745
|
+
const set = /* @__PURE__ */ new Set();
|
|
746
|
+
const ENTRY_SIZE = 48 + 48 + 48 + 32 + 8;
|
|
747
|
+
for (let i = 0; i < vecLen; i++) {
|
|
748
|
+
const pcr0 = data.subarray(off, off + 48);
|
|
749
|
+
set.add(toHex(pcr0));
|
|
750
|
+
off += ENTRY_SIZE;
|
|
751
|
+
}
|
|
752
|
+
return set;
|
|
753
|
+
}
|
|
754
|
+
function toHex(bytes) {
|
|
755
|
+
let s = "";
|
|
756
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
757
|
+
s += bytes[i].toString(16).padStart(2, "0");
|
|
758
|
+
}
|
|
759
|
+
return s;
|
|
760
|
+
}
|
|
761
|
+
function bytesEqual(a, b) {
|
|
762
|
+
if (a.length !== b.length) return false;
|
|
763
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
764
|
+
return true;
|
|
765
|
+
}
|
|
766
|
+
function bs58Encode(bytes) {
|
|
767
|
+
const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
768
|
+
let n = 0n;
|
|
769
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
770
|
+
n = (n << 8n) + BigInt(bytes[i]);
|
|
771
|
+
}
|
|
772
|
+
let out = "";
|
|
773
|
+
while (n > 0n) {
|
|
774
|
+
const rem = Number(n % 58n);
|
|
775
|
+
n = n / 58n;
|
|
776
|
+
out = ALPHABET[rem] + out;
|
|
777
|
+
}
|
|
778
|
+
for (let i = 0; i < bytes.length && bytes[i] === 0; i++) {
|
|
779
|
+
out = "1" + out;
|
|
780
|
+
}
|
|
781
|
+
return out;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// src/client/LoopByoaaClient.ts
|
|
785
|
+
var LoopByoaaError = class extends Error {
|
|
786
|
+
constructor(code, message, context = {}) {
|
|
787
|
+
super(message);
|
|
788
|
+
this.name = "LoopByoaaError";
|
|
789
|
+
this.code = code;
|
|
790
|
+
this.status = context.status;
|
|
791
|
+
this.request_id = context.request_id;
|
|
792
|
+
this.documentation_url = context.documentation_url;
|
|
793
|
+
this.details = context.details;
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
var LoopByoaaApiKeyError = class extends LoopByoaaError {
|
|
797
|
+
constructor(code, message, context = {}) {
|
|
798
|
+
super(code, message, context);
|
|
799
|
+
this.name = "LoopByoaaApiKeyError";
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
var LoopByoaaPermissionError = class extends LoopByoaaError {
|
|
803
|
+
constructor(code, message, context = {}) {
|
|
804
|
+
super(code, message, context);
|
|
805
|
+
this.name = "LoopByoaaPermissionError";
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
var LoopByoaaRateLimitError = class extends LoopByoaaError {
|
|
809
|
+
constructor(code, message, context = {}) {
|
|
810
|
+
super(code, message, context);
|
|
811
|
+
this.name = "LoopByoaaRateLimitError";
|
|
812
|
+
this.retry_after_seconds = context.retry_after_seconds;
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
var LoopByoaaQuotaError = class extends LoopByoaaError {
|
|
816
|
+
constructor(code, message, context = {}) {
|
|
817
|
+
super(code, message, context);
|
|
818
|
+
this.name = "LoopByoaaQuotaError";
|
|
819
|
+
this.meter_name = context.meter_name;
|
|
820
|
+
this.upgrade_url = context.upgrade_url;
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
var LoopByoaaValidationError = class extends LoopByoaaError {
|
|
824
|
+
constructor(code, message, context = {}) {
|
|
825
|
+
super(code, message, context);
|
|
826
|
+
this.name = "LoopByoaaValidationError";
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
var LoopByoaaServerError = class extends LoopByoaaError {
|
|
830
|
+
constructor(code, message, context = {}) {
|
|
831
|
+
super(code, message, context);
|
|
832
|
+
this.name = "LoopByoaaServerError";
|
|
833
|
+
}
|
|
834
|
+
};
|
|
835
|
+
var LoopByoaaNetworkError = class extends LoopByoaaError {
|
|
836
|
+
constructor(message, context = {}) {
|
|
837
|
+
super("network_error", message, context);
|
|
838
|
+
this.name = "LoopByoaaNetworkError";
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
var ENV_BASE_URLS = {
|
|
842
|
+
dev: "https://dev-api.kya.looplocal.io",
|
|
843
|
+
staging: "https://staging-api.kya.looplocal.io",
|
|
844
|
+
prod: "https://prod-api.kya.loopprotocol.com"
|
|
845
|
+
};
|
|
846
|
+
function baseUrlForEnv(env) {
|
|
847
|
+
return ENV_BASE_URLS[env];
|
|
848
|
+
}
|
|
849
|
+
function parseLoopByoaaSdkKey(apiKey) {
|
|
850
|
+
const match = /^loop_byoaa_(dev|staging|prod)_([A-Za-z0-9]{32})$/.exec(apiKey);
|
|
851
|
+
if (!match) {
|
|
852
|
+
throw new LoopByoaaApiKeyError(
|
|
853
|
+
"invalid_api_key",
|
|
854
|
+
"Loop BYOAA SDK keys must match loop_byoaa_<dev|staging|prod>_<32 base62 characters>"
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
const env = match[1];
|
|
858
|
+
return {
|
|
859
|
+
env,
|
|
860
|
+
prefix: `loop_byoaa_${env}`,
|
|
861
|
+
hashable: apiKey
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
function envForKnownBaseUrl(baseUrl) {
|
|
865
|
+
const cleaned = cleanBaseUrl(baseUrl);
|
|
866
|
+
return Object.entries(ENV_BASE_URLS).find(
|
|
867
|
+
([, knownUrl]) => cleanBaseUrl(knownUrl) === cleaned
|
|
868
|
+
)?.[0];
|
|
869
|
+
}
|
|
870
|
+
function cleanBaseUrl(baseUrl) {
|
|
871
|
+
return baseUrl.replace(/\/+$/, "");
|
|
872
|
+
}
|
|
873
|
+
function encodeValue(value) {
|
|
874
|
+
if (value instanceof Date) return value.toISOString();
|
|
875
|
+
return String(value);
|
|
876
|
+
}
|
|
877
|
+
function toQuery(params) {
|
|
878
|
+
if (!params) return "";
|
|
879
|
+
const query = new URLSearchParams();
|
|
880
|
+
for (const [key, value] of Object.entries(params)) {
|
|
881
|
+
if (value === void 0 || value === null) continue;
|
|
882
|
+
query.set(key, encodeValue(value));
|
|
883
|
+
}
|
|
884
|
+
const encoded = query.toString();
|
|
885
|
+
return encoded ? `?${encoded}` : "";
|
|
886
|
+
}
|
|
887
|
+
function normalizeBody(value) {
|
|
888
|
+
if (value instanceof Date) return value.toISOString();
|
|
889
|
+
if (Array.isArray(value)) return value.map(normalizeBody);
|
|
890
|
+
if (value && typeof value === "object") {
|
|
891
|
+
return Object.fromEntries(
|
|
892
|
+
Object.entries(value).map(([key, entry]) => [key, normalizeBody(entry)])
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
return value;
|
|
896
|
+
}
|
|
897
|
+
function readHeader(headers, name) {
|
|
898
|
+
return headers.get(name) ?? headers.get(name.toLowerCase()) ?? void 0;
|
|
899
|
+
}
|
|
900
|
+
function errorFromResponse(status, payload, headers) {
|
|
901
|
+
const nested = payload.error && typeof payload.error === "object" && !Array.isArray(payload.error) ? payload.error : void 0;
|
|
902
|
+
const envelope = nested ?? payload;
|
|
903
|
+
const details = envelope.details && typeof envelope.details === "object" && !Array.isArray(envelope.details) ? envelope.details : void 0;
|
|
904
|
+
const code = String(envelope.code ?? (status >= 500 ? "server_error" : "unknown"));
|
|
905
|
+
const message = String(envelope.message ?? `Loop BYOAA request failed with HTTP ${status}`);
|
|
906
|
+
const retryAfterSeconds = typeof details?.retry_after_seconds === "number" ? details.retry_after_seconds : typeof envelope.retry_after_seconds === "number" ? envelope.retry_after_seconds : Number(readHeader(headers, "retry-after")) || void 0;
|
|
907
|
+
const context = {
|
|
908
|
+
status,
|
|
909
|
+
request_id: typeof envelope.request_id === "string" ? envelope.request_id : readHeader(headers, "x-request-id"),
|
|
910
|
+
documentation_url: typeof envelope.documentation_url === "string" ? envelope.documentation_url : void 0,
|
|
911
|
+
retry_after_seconds: retryAfterSeconds,
|
|
912
|
+
meter_name: typeof envelope.meter_name === "string" ? envelope.meter_name : void 0,
|
|
913
|
+
upgrade_url: typeof envelope.upgrade_url === "string" ? envelope.upgrade_url : void 0,
|
|
914
|
+
details: envelope.details
|
|
915
|
+
};
|
|
916
|
+
if (status === 401 || status === 403 || code === "auth" || code === "invalid_api_key") {
|
|
917
|
+
return status === 403 || code === "permission_denied" ? new LoopByoaaPermissionError(code, message, context) : new LoopByoaaApiKeyError(code, message, context);
|
|
918
|
+
}
|
|
919
|
+
if (status === 400 || status === 422 || code === "validation") return new LoopByoaaValidationError(code, message, context);
|
|
920
|
+
if (status === 429 || code === "rate_limit") return new LoopByoaaRateLimitError(code, message, context);
|
|
921
|
+
if (status === 402 || code === "quota") return new LoopByoaaQuotaError(code, message, context);
|
|
922
|
+
if (status >= 500) return new LoopByoaaServerError(code, message, context);
|
|
923
|
+
return new LoopByoaaError(code, message, context);
|
|
924
|
+
}
|
|
925
|
+
var HttpClient = class {
|
|
926
|
+
constructor(apiKey, baseUrl, fetchImpl, timeout) {
|
|
927
|
+
this.apiKey = apiKey;
|
|
928
|
+
this.baseUrl = baseUrl;
|
|
929
|
+
this.fetchImpl = fetchImpl;
|
|
930
|
+
this.timeout = timeout;
|
|
931
|
+
}
|
|
932
|
+
async request(method, path, body, query) {
|
|
933
|
+
const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
|
|
934
|
+
const timer = controller ? setTimeout(() => controller.abort(), this.timeout) : void 0;
|
|
935
|
+
const hasBody = body !== void 0;
|
|
936
|
+
const headers = {
|
|
937
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
938
|
+
Accept: "application/json"
|
|
939
|
+
};
|
|
940
|
+
if (hasBody) headers["Content-Type"] = "application/json";
|
|
941
|
+
try {
|
|
942
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path}${toQuery(query)}`, {
|
|
943
|
+
method,
|
|
944
|
+
headers,
|
|
945
|
+
body: hasBody ? JSON.stringify(normalizeBody(body)) : void 0,
|
|
946
|
+
signal: controller?.signal
|
|
947
|
+
});
|
|
948
|
+
if (timer) clearTimeout(timer);
|
|
949
|
+
const text = await response.text();
|
|
950
|
+
const payload = text ? JSON.parse(text) : void 0;
|
|
951
|
+
if (!response.ok) {
|
|
952
|
+
throw errorFromResponse(response.status, payload ?? {}, response.headers);
|
|
953
|
+
}
|
|
954
|
+
return payload;
|
|
955
|
+
} catch (error) {
|
|
956
|
+
if (timer) clearTimeout(timer);
|
|
957
|
+
if (error instanceof LoopByoaaError) throw error;
|
|
958
|
+
throw new LoopByoaaNetworkError(error instanceof Error ? error.message : "Loop BYOAA network request failed");
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
};
|
|
962
|
+
function unwrapGatewayEnvelope(payload, key) {
|
|
963
|
+
if (payload && typeof payload === "object" && !Array.isArray(payload) && key in payload) {
|
|
964
|
+
return payload[key];
|
|
965
|
+
}
|
|
966
|
+
return payload;
|
|
967
|
+
}
|
|
968
|
+
function unwrapGatewayList(payload, key) {
|
|
969
|
+
if (payload && typeof payload === "object" && !Array.isArray(payload)) {
|
|
970
|
+
const record = payload;
|
|
971
|
+
if (Array.isArray(record[key])) return { items: record[key] };
|
|
972
|
+
if (Array.isArray(record.items)) return record;
|
|
973
|
+
}
|
|
974
|
+
return payload;
|
|
975
|
+
}
|
|
976
|
+
var LoopByoaaClient = class {
|
|
977
|
+
constructor(options) {
|
|
978
|
+
if (!options.apiKey) {
|
|
979
|
+
throw new LoopByoaaApiKeyError("auth", "Loop BYOAA apiKey is required");
|
|
980
|
+
}
|
|
981
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
982
|
+
if (!fetchImpl) {
|
|
983
|
+
throw new LoopByoaaNetworkError("Loop BYOAA requires a fetch implementation in this runtime");
|
|
984
|
+
}
|
|
985
|
+
const parsedKey = parseLoopByoaaSdkKey(options.apiKey);
|
|
986
|
+
const requestedBaseUrl = options.baseUrl ?? baseUrlForEnv(parsedKey.env);
|
|
987
|
+
const knownEnv = envForKnownBaseUrl(requestedBaseUrl);
|
|
988
|
+
if (knownEnv && knownEnv !== parsedKey.env) {
|
|
989
|
+
throw new LoopByoaaApiKeyError(
|
|
990
|
+
"cross_env_key_use",
|
|
991
|
+
`Loop BYOAA ${parsedKey.env} keys cannot call ${knownEnv} endpoints`,
|
|
992
|
+
{ details: { key_env: parsedKey.env, endpoint_env: knownEnv } }
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
this.apiKey = options.apiKey;
|
|
996
|
+
this.env = parsedKey.env;
|
|
997
|
+
this.parsedKey = parsedKey;
|
|
998
|
+
this.baseUrl = cleanBaseUrl(requestedBaseUrl);
|
|
999
|
+
const http = new HttpClient(this.apiKey, this.baseUrl, fetchImpl, options.timeout ?? 1e4);
|
|
1000
|
+
this.agents = new AgentsNamespace(http);
|
|
1001
|
+
this.permissions = new PermissionsNamespace(http);
|
|
1002
|
+
this.decisions = new DecisionsNamespace(http);
|
|
1003
|
+
this.audit = new AuditNamespace(http);
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
var AgentsNamespace = class {
|
|
1007
|
+
constructor(http) {
|
|
1008
|
+
this.http = http;
|
|
1009
|
+
}
|
|
1010
|
+
register(input) {
|
|
1011
|
+
return this.http.request("POST", "/api/v1/kya/agents/register", input).then((payload) => unwrapGatewayEnvelope(payload, "agent"));
|
|
1012
|
+
}
|
|
1013
|
+
list(params = {}) {
|
|
1014
|
+
return this.http.request("GET", "/api/v1/kya/agents", void 0, params).then((payload) => unwrapGatewayList(payload, "agents"));
|
|
1015
|
+
}
|
|
1016
|
+
get(agentIdOrExternalId) {
|
|
1017
|
+
return this.http.request("GET", `/api/v1/kya/agents/${encodeURIComponent(agentIdOrExternalId)}`).then((payload) => unwrapGatewayEnvelope(payload, "agent"));
|
|
1018
|
+
}
|
|
1019
|
+
update(id, input) {
|
|
1020
|
+
return this.http.request("PATCH", `/api/v1/kya/agents/${encodeURIComponent(id)}`, input);
|
|
1021
|
+
}
|
|
1022
|
+
suspend(id, reason) {
|
|
1023
|
+
return this.http.request("POST", `/api/v1/kya/agents/${encodeURIComponent(id)}/suspend`, { reason });
|
|
1024
|
+
}
|
|
1025
|
+
revoke(id, reason) {
|
|
1026
|
+
return this.http.request("POST", `/api/v1/kya/agents/${encodeURIComponent(id)}/revoke`, { reason });
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
var PermissionsNamespace = class {
|
|
1030
|
+
constructor(http) {
|
|
1031
|
+
this.http = http;
|
|
1032
|
+
}
|
|
1033
|
+
grant(input) {
|
|
1034
|
+
return this.http.request("POST", "/api/v1/kya/permissions/grant", input).then((payload) => unwrapGatewayEnvelope(payload, "permission"));
|
|
1035
|
+
}
|
|
1036
|
+
list(params = {}) {
|
|
1037
|
+
return this.http.request("GET", "/api/v1/kya/permissions", void 0, params).then((payload) => unwrapGatewayList(payload, "permissions"));
|
|
1038
|
+
}
|
|
1039
|
+
get(grantId) {
|
|
1040
|
+
return this.http.request("GET", `/api/v1/kya/permissions/${encodeURIComponent(grantId)}`).then((payload) => unwrapGatewayEnvelope(payload, "permission"));
|
|
1041
|
+
}
|
|
1042
|
+
revoke(grantId, input) {
|
|
1043
|
+
return this.http.request("POST", `/api/v1/kya/permissions/${encodeURIComponent(grantId)}/revoke`, input);
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
var DecisionsNamespace = class {
|
|
1047
|
+
constructor(http) {
|
|
1048
|
+
this.http = http;
|
|
1049
|
+
}
|
|
1050
|
+
request(input) {
|
|
1051
|
+
return this.http.request("POST", "/api/v1/kya/decisions/request", input);
|
|
1052
|
+
}
|
|
1053
|
+
get(envelopeId) {
|
|
1054
|
+
return this.http.request("GET", `/api/v1/kya/decisions/${encodeURIComponent(envelopeId)}`).then((payload) => unwrapGatewayEnvelope(payload, "decision"));
|
|
1055
|
+
}
|
|
1056
|
+
list(params = {}) {
|
|
1057
|
+
return this.http.request("GET", "/api/v1/kya/decisions", void 0, params).then((payload) => unwrapGatewayList(payload, "decisions"));
|
|
1058
|
+
}
|
|
1059
|
+
};
|
|
1060
|
+
var AuditNamespace = class {
|
|
1061
|
+
constructor(http) {
|
|
1062
|
+
this.http = http;
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Export a tamper-evident KYA audit pack.
|
|
1066
|
+
*
|
|
1067
|
+
* TODO(F.5): switch this to the public audit-pack REST route once the
|
|
1068
|
+
* customer-facing endpoint ships. E.1 intentionally points at the current
|
|
1069
|
+
* internal export bridge so the method shape is stable for quickstart code.
|
|
1070
|
+
*/
|
|
1071
|
+
exportPack(input) {
|
|
1072
|
+
return this.http.request("GET", "/api/v1/kya/audit/export-pack", void 0, { limit: 200 }).then((payload) => unwrapGatewayEnvelope(payload, "export_pack"));
|
|
1073
|
+
}
|
|
1074
|
+
export(input) {
|
|
1075
|
+
return this.exportPack(input);
|
|
1076
|
+
}
|
|
1077
|
+
list(params = {}) {
|
|
1078
|
+
return this.http.request("GET", "/api/v1/kya/audit/events", void 0, params).then((payload) => unwrapGatewayList(payload, "events"));
|
|
1079
|
+
}
|
|
1080
|
+
get(eventId) {
|
|
1081
|
+
return this.http.request("GET", `/api/v1/kya/audit/events/${encodeURIComponent(eventId)}`).then((payload) => unwrapGatewayEnvelope(payload, "event"));
|
|
1082
|
+
}
|
|
1083
|
+
};
|
|
1084
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1085
|
+
0 && (module.exports = {
|
|
1086
|
+
AgentsNamespace,
|
|
1087
|
+
AttestedReceiptSubmitter,
|
|
1088
|
+
AttestedReceiptVerifier,
|
|
1089
|
+
AuditNamespace,
|
|
1090
|
+
DEVNET_PROGRAM_IDS,
|
|
1091
|
+
DecisionsNamespace,
|
|
1092
|
+
LoopByoaaApiKeyError,
|
|
1093
|
+
LoopByoaaClient,
|
|
1094
|
+
LoopByoaaError,
|
|
1095
|
+
LoopByoaaNetworkError,
|
|
1096
|
+
LoopByoaaPermissionError,
|
|
1097
|
+
LoopByoaaQuotaError,
|
|
1098
|
+
LoopByoaaRateLimitError,
|
|
1099
|
+
LoopByoaaServerError,
|
|
1100
|
+
LoopByoaaValidationError,
|
|
1101
|
+
MAINNET_PROGRAM_IDS,
|
|
1102
|
+
MAX_MERCHANT_NAME_RAW_LEN,
|
|
1103
|
+
MAX_RECEIPT_AGE_SECONDS,
|
|
1104
|
+
MAX_RECEIPT_CENTS,
|
|
1105
|
+
PermissionsNamespace,
|
|
1106
|
+
ReceiptNotAuditedError,
|
|
1107
|
+
ReceiptNotFoundError,
|
|
1108
|
+
SESSION_INSTRUCTION_INDEX_SUBMIT_ATTESTED_RECEIPT,
|
|
1109
|
+
UnsupportedProofTypeError,
|
|
1110
|
+
decodeBankAttestedReceipt,
|
|
1111
|
+
decodeRegistryPcr0Set,
|
|
1112
|
+
defaultRpcUrl,
|
|
1113
|
+
deriveAgentSessionPda,
|
|
1114
|
+
deriveBankAttestedReceiptPda,
|
|
1115
|
+
deriveBankTxnId,
|
|
1116
|
+
deriveShoppingStatePda,
|
|
1117
|
+
encodeAttestedReceiptArgs,
|
|
1118
|
+
encodeSubmitAttestedReceiptIxData,
|
|
1119
|
+
getProgramIds,
|
|
1120
|
+
instructionDiscriminator,
|
|
1121
|
+
isMainnet,
|
|
1122
|
+
normalizeMerchantName,
|
|
1123
|
+
parseLoopByoaaSdkKey,
|
|
1124
|
+
resolveConfig,
|
|
1125
|
+
validateReceipt,
|
|
1126
|
+
verifyAttestedReceiptProof
|
|
1127
|
+
});
|