@infantmen-labs/harbor-sdk 0.5.0 → 0.6.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/README.md CHANGED
@@ -35,8 +35,11 @@ complete third-party monitor built on SDK + web3.js only.
35
35
  ## Buyer patterns (demand-side integration)
36
36
 
37
37
  Upstream channel builders (`openChannelIx` / `topUpIx` / `settleIx`,
38
- `deriveChannel`) live here so buyers never reimplement
39
- consensus-critical bytes by copy-paste. The proven demand-side flow:
38
+ `deriveChannel`, plus the close lifecycle `requestCloseIx` / `sealIx` /
39
+ `withdrawPayerIx` / `distributeIx` / `reclaimIx`) live here so buyers
40
+ never reimplement consensus-critical bytes by copy-paste. `vaultAta`,
41
+ `decodeBinding`, and receipt-expiry readers round out the buyer surface.
42
+ The proven demand-side flow:
40
43
 
41
44
  1. **Gate on collateral.** Derive `bondPda(merchant, mint)` offline,
42
45
  `decodeBond` it, refuse when `amount − reserved` is below your
@@ -26,6 +26,65 @@ export declare function topUpIx(args: {
26
26
  mint: PublicKey;
27
27
  amount: bigint;
28
28
  }): TransactionInstruction;
29
+ /**
30
+ * Upstream forced-close lifecycle (buyer reclaim path). All no-arg beyond
31
+ * the discriminator; discriminators from the pinned upstream IDL
32
+ * (requestClose 5, seal 6, withdrawPayer 8, reclaim 9).
33
+ *
34
+ * Reclaim flow: `requestClose` (payer starts grace) → wait past grace →
35
+ * `seal` → `withdrawPayer` (unspent remainder back to the payer) →
36
+ * `reclaim` (deallocate channel, rent to rentPayer). Cooperative
37
+ * alternative: `settleAndSeal` (needs payee signature — not built here).
38
+ */
39
+ export declare function requestCloseIx(args: {
40
+ programId: PublicKey;
41
+ payer: PublicKey;
42
+ channel: PublicKey;
43
+ }): TransactionInstruction;
44
+ export declare function sealIx(args: {
45
+ programId: PublicKey;
46
+ channel: PublicKey;
47
+ }): TransactionInstruction;
48
+ export declare function withdrawPayerIx(args: {
49
+ programId: PublicKey;
50
+ payer: PublicKey;
51
+ channel: PublicKey;
52
+ channelAta: PublicKey;
53
+ payerAta: PublicKey;
54
+ mint: PublicKey;
55
+ }): TransactionInstruction;
56
+ export declare function reclaimIx(args: {
57
+ programId: PublicKey;
58
+ channel: PublicKey;
59
+ rentPayer: PublicKey;
60
+ }): TransactionInstruction;
61
+ /**
62
+ * Upstream `distribute` with an explicit recipient list (empty = payee
63
+ * remainder only). Data: disc(7) | count(u32 LE) | entries. The onchain
64
+ * program rehashes the revealed plan and compares against the commitment
65
+ * stored at `open` — agent-opened channels commit to the empty plan
66
+ * (count 0), so empty args verify for them.
67
+ *
68
+ * Fixed accounts: channel(mut) | payer | rentPayer(mut) | channelAta(mut)
69
+ * | payerAta(mut) | payeeAta(mut) | treasuryAta(mut) | mint |
70
+ * tokenProgram | eventAuthority | selfProgram, then recipient ATAs in plan
71
+ * order. Permissionless crank: payer is deliberately NOT marked signer
72
+ * (the upstream docs require payer-side signatures only for topUp,
73
+ * requestClose and withdrawPayer — the fee payer signs the tx itself).
74
+ */
75
+ export declare function distributeIx(args: {
76
+ programId: PublicKey;
77
+ channel: PublicKey;
78
+ payer: PublicKey;
79
+ rentPayer: PublicKey;
80
+ channelAta: PublicKey;
81
+ payerAta: PublicKey;
82
+ payeeAta: PublicKey;
83
+ treasuryAta: PublicKey;
84
+ mint: PublicKey;
85
+ eventAuthority: PublicKey;
86
+ recipientAtas?: PublicKey[];
87
+ }): TransactionInstruction;
29
88
  export declare function deriveChannel(programId: PublicKey, payer: PublicKey, payee: PublicKey, mint: PublicKey, authorizedSigner: PublicKey, salt: bigint, openSlot: bigint): {
30
89
  channel: PublicKey;
31
90
  };
@@ -3,6 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.openChannelIx = openChannelIx;
4
4
  exports.settleIx = settleIx;
5
5
  exports.topUpIx = topUpIx;
6
+ exports.requestCloseIx = requestCloseIx;
7
+ exports.sealIx = sealIx;
8
+ exports.withdrawPayerIx = withdrawPayerIx;
9
+ exports.reclaimIx = reclaimIx;
10
+ exports.distributeIx = distributeIx;
6
11
  exports.deriveChannel = deriveChannel;
7
12
  const web3_js_1 = require("@solana/web3.js");
8
13
  const ids_1 = require("./ids");
@@ -75,6 +80,86 @@ function topUpIx(args) {
75
80
  data: Buffer.concat([Buffer.from([3]), (0, u64_1.u64le)(args.amount)]),
76
81
  });
77
82
  }
83
+ /**
84
+ * Upstream forced-close lifecycle (buyer reclaim path). All no-arg beyond
85
+ * the discriminator; discriminators from the pinned upstream IDL
86
+ * (requestClose 5, seal 6, withdrawPayer 8, reclaim 9).
87
+ *
88
+ * Reclaim flow: `requestClose` (payer starts grace) → wait past grace →
89
+ * `seal` → `withdrawPayer` (unspent remainder back to the payer) →
90
+ * `reclaim` (deallocate channel, rent to rentPayer). Cooperative
91
+ * alternative: `settleAndSeal` (needs payee signature — not built here).
92
+ */
93
+ function requestCloseIx(args) {
94
+ return new web3_js_1.TransactionInstruction({
95
+ programId: args.programId,
96
+ keys: [m(args.payer, false, true), m(args.channel, true, false)],
97
+ data: Buffer.from([5]),
98
+ });
99
+ }
100
+ function sealIx(args) {
101
+ return new web3_js_1.TransactionInstruction({
102
+ programId: args.programId,
103
+ keys: [m(args.channel, true, false)],
104
+ data: Buffer.from([6]),
105
+ });
106
+ }
107
+ function withdrawPayerIx(args) {
108
+ return new web3_js_1.TransactionInstruction({
109
+ programId: args.programId,
110
+ keys: [
111
+ m(args.payer, false, true),
112
+ m(args.channel, true, false),
113
+ m(args.channelAta, true, false),
114
+ m(args.payerAta, true, false),
115
+ m(args.mint, false, false),
116
+ m(ids_1.TOKEN_PROGRAM_ID, false, false),
117
+ ],
118
+ data: Buffer.from([8]),
119
+ });
120
+ }
121
+ function reclaimIx(args) {
122
+ return new web3_js_1.TransactionInstruction({
123
+ programId: args.programId,
124
+ keys: [m(args.channel, true, false), m(args.rentPayer, true, false)],
125
+ data: Buffer.from([9]),
126
+ });
127
+ }
128
+ /**
129
+ * Upstream `distribute` with an explicit recipient list (empty = payee
130
+ * remainder only). Data: disc(7) | count(u32 LE) | entries. The onchain
131
+ * program rehashes the revealed plan and compares against the commitment
132
+ * stored at `open` — agent-opened channels commit to the empty plan
133
+ * (count 0), so empty args verify for them.
134
+ *
135
+ * Fixed accounts: channel(mut) | payer | rentPayer(mut) | channelAta(mut)
136
+ * | payerAta(mut) | payeeAta(mut) | treasuryAta(mut) | mint |
137
+ * tokenProgram | eventAuthority | selfProgram, then recipient ATAs in plan
138
+ * order. Permissionless crank: payer is deliberately NOT marked signer
139
+ * (the upstream docs require payer-side signatures only for topUp,
140
+ * requestClose and withdrawPayer — the fee payer signs the tx itself).
141
+ */
142
+ function distributeIx(args) {
143
+ const data = Buffer.concat([Buffer.from([7]), u32(0)]);
144
+ return new web3_js_1.TransactionInstruction({
145
+ programId: args.programId,
146
+ keys: [
147
+ m(args.channel, true, false),
148
+ m(args.payer, false, false),
149
+ m(args.rentPayer, true, false),
150
+ m(args.channelAta, true, false),
151
+ m(args.payerAta, true, false),
152
+ m(args.payeeAta, true, false),
153
+ m(args.treasuryAta, true, false),
154
+ m(args.mint, false, false),
155
+ m(ids_1.TOKEN_PROGRAM_ID, false, false),
156
+ m(args.eventAuthority, false, false),
157
+ m(args.programId, false, false),
158
+ ...(args.recipientAtas ?? []).map((a) => m(a, true, false)),
159
+ ],
160
+ data,
161
+ });
162
+ }
78
163
  function deriveChannel(programId, payer, payee, mint, authorizedSigner, salt, openSlot) {
79
164
  const [channel] = (0, pda_1.channelPda)(programId, payer, payee, mint, authorizedSigner, salt, openSlot);
80
165
  return { channel };
@@ -16,5 +16,11 @@ export interface SubmitReceiptArgs {
16
16
  }
17
17
  export declare function submitReceiptIx(programId: PublicKey, merchant: PublicKey, bond: PublicKey, binding: PublicKey, receipt: PublicKey, args: SubmitReceiptArgs): TransactionInstruction;
18
18
  export declare function openDisputeIx(programId: PublicKey, claimant: PublicKey, bond: PublicKey, binding: PublicKey, channel: PublicKey, dispute: PublicKey, claim: PublicKey, mint: PublicKey, claimantAta: PublicKey, vault: PublicKey, nonce: bigint, reason: number, claimSpend: bigint): TransactionInstruction;
19
+ /**
20
+ * Closes an EMPTY bond (amount == 0, no open disputes) and its empty
21
+ * vault. Discriminator already tabled above (method-validated against
22
+ * submit_receipt); accounts mirror RefundUnused in the program.
23
+ */
24
+ export declare function refundUnusedIx(programId: PublicKey, merchant: PublicKey, bond: PublicKey, mint: PublicKey, vault: PublicKey, merchantAta: PublicKey): TransactionInstruction;
19
25
  export declare function haltBindingIx(programId: PublicKey, merchant: PublicKey, binding: PublicKey): TransactionInstruction;
20
26
  export declare function resolveTimeoutIx(programId: PublicKey, resolver: PublicKey, bond: PublicKey, mint: PublicKey, binding: PublicKey, dispute: PublicKey, claimant: PublicKey, treasury: PublicKey, vault: PublicKey, treasuryAta: PublicKey, claimantAta: PublicKey, nonce: bigint): TransactionInstruction;
@@ -8,6 +8,7 @@ exports.withdrawTreasuryIx = withdrawTreasuryIx;
8
8
  exports.bindChannelIx = bindChannelIx;
9
9
  exports.submitReceiptIx = submitReceiptIx;
10
10
  exports.openDisputeIx = openDisputeIx;
11
+ exports.refundUnusedIx = refundUnusedIx;
11
12
  exports.haltBindingIx = haltBindingIx;
12
13
  exports.resolveTimeoutIx = resolveTimeoutIx;
13
14
  const web3_js_1 = require("@solana/web3.js");
@@ -151,6 +152,21 @@ function openDisputeIx(programId, claimant, bond, binding, channel, dispute, cla
151
152
  { key: ids_1.SYSTEM_PROGRAM_ID },
152
153
  ]);
153
154
  }
155
+ /**
156
+ * Closes an EMPTY bond (amount == 0, no open disputes) and its empty
157
+ * vault. Discriminator already tabled above (method-validated against
158
+ * submit_receipt); accounts mirror RefundUnused in the program.
159
+ */
160
+ function refundUnusedIx(programId, merchant, bond, mint, vault, merchantAta) {
161
+ return keys(programId, D.refundUnused, Buffer.alloc(0), [
162
+ { key: merchant, w: true, s: true },
163
+ { key: bond, w: true },
164
+ { key: mint },
165
+ { key: vault, w: true },
166
+ { key: merchantAta, w: true },
167
+ { key: ids_1.TOKEN_PROGRAM_ID, w: false },
168
+ ]);
169
+ }
154
170
  function haltBindingIx(programId, merchant, binding) {
155
171
  return keys(programId, D.haltBinding, Buffer.alloc(0), [
156
172
  { key: merchant, w: true, s: true },
package/dist/src/pda.d.ts CHANGED
@@ -9,4 +9,6 @@ export declare function claimPda(binding: PublicKey, nonce: bigint): [PublicKey,
9
9
  export declare function channelPda(channelProgram: PublicKey, payer: PublicKey, payee: PublicKey, mint: PublicKey, authorizedSigner: PublicKey, salt: bigint, openSlot: bigint): [PublicKey, number];
10
10
  /** Canonical associated token address for (owner, mint). */
11
11
  export declare function ataFor(owner: PublicKey, mint: PublicKey): PublicKey;
12
+ /** Bond vault: the canonical ATA of the bond PDA (holds collateral). */
13
+ export declare function vaultAta(bond: PublicKey, mint: PublicKey): PublicKey;
12
14
  export declare function channelAta(channel: PublicKey, tokenProgram: PublicKey, mint: PublicKey, ataProgram: PublicKey): [PublicKey, number];
package/dist/src/pda.js CHANGED
@@ -8,6 +8,7 @@ exports.treasuryPda = treasuryPda;
8
8
  exports.claimPda = claimPda;
9
9
  exports.channelPda = channelPda;
10
10
  exports.ataFor = ataFor;
11
+ exports.vaultAta = vaultAta;
11
12
  exports.channelAta = channelAta;
12
13
  const web3_js_1 = require("@solana/web3.js");
13
14
  const ids_1 = require("./ids");
@@ -52,6 +53,10 @@ function channelPda(channelProgram, payer, payee, mint, authorizedSigner, salt,
52
53
  function ataFor(owner, mint) {
53
54
  return web3_js_1.PublicKey.findProgramAddressSync([owner.toBuffer(), ids_2.TOKEN_PROGRAM_ID.toBuffer(), mint.toBuffer()], ids_2.ATA_PROGRAM_ID)[0];
54
55
  }
56
+ /** Bond vault: the canonical ATA of the bond PDA (holds collateral). */
57
+ function vaultAta(bond, mint) {
58
+ return ataFor(bond, mint);
59
+ }
55
60
  function channelAta(channel, tokenProgram, mint, ataProgram) {
56
61
  return web3_js_1.PublicKey.findProgramAddressSync([channel.toBuffer(), tokenProgram.toBuffer(), mint.toBuffer()], ataProgram);
57
62
  }
@@ -13,3 +13,17 @@ export interface ReceiptFields {
13
13
  /** Borsh encoding of the onchain ReceiptMessage. Field order is consensus. */
14
14
  export declare function receiptMessageBytes(f: ReceiptFields): Buffer;
15
15
  export declare const RECEIPT_MESSAGE_LEN = 185;
16
+ /**
17
+ * Byte offset of expirySlot in the 185-byte receipt message
18
+ * (merchant 32 | binding 32 | cumulative 8 | meter 32 | output 32 |
19
+ * status 1 | nonce 8 | expiry 8 | signer 32).
20
+ */
21
+ export declare const RECEIPT_EXPIRY_OFFSET = 145;
22
+ /** Reads the expiry slot from encoded receipt message bytes. */
23
+ export declare function receiptExpirySlot(message: Uint8Array): bigint;
24
+ /**
25
+ * Buyer-side expiry check: a receipt is expired once the chain passes its
26
+ * expiry slot (mirrors the onchain `slot <= expiry_slot` gate in
27
+ * submit_receipt — expired receipts fail submit with `Expired`).
28
+ */
29
+ export declare function isReceiptExpired(message: Uint8Array, slot: bigint | number): boolean;
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.RECEIPT_MESSAGE_LEN = void 0;
3
+ exports.RECEIPT_EXPIRY_OFFSET = exports.RECEIPT_MESSAGE_LEN = void 0;
4
4
  exports.receiptMessageBytes = receiptMessageBytes;
5
+ exports.receiptExpirySlot = receiptExpirySlot;
6
+ exports.isReceiptExpired = isReceiptExpired;
5
7
  const u64_1 = require("./u64");
6
8
  function assertLen(b, n, name) {
7
9
  if (b.length !== n)
@@ -34,3 +36,25 @@ function receiptMessageBytes(f) {
34
36
  return out;
35
37
  }
36
38
  exports.RECEIPT_MESSAGE_LEN = 185;
39
+ /**
40
+ * Byte offset of expirySlot in the 185-byte receipt message
41
+ * (merchant 32 | binding 32 | cumulative 8 | meter 32 | output 32 |
42
+ * status 1 | nonce 8 | expiry 8 | signer 32).
43
+ */
44
+ exports.RECEIPT_EXPIRY_OFFSET = 145;
45
+ /** Reads the expiry slot from encoded receipt message bytes. */
46
+ function receiptExpirySlot(message) {
47
+ let v = 0n;
48
+ for (let i = 0; i < 8; i++) {
49
+ v |= BigInt(message[exports.RECEIPT_EXPIRY_OFFSET + i]) << BigInt(i * 8);
50
+ }
51
+ return v;
52
+ }
53
+ /**
54
+ * Buyer-side expiry check: a receipt is expired once the chain passes its
55
+ * expiry slot (mirrors the onchain `slot <= expiry_slot` gate in
56
+ * submit_receipt — expired receipts fail submit with `Expired`).
57
+ */
58
+ function isReceiptExpired(message, slot) {
59
+ return BigInt(slot) > receiptExpirySlot(message);
60
+ }
@@ -432,3 +432,93 @@ const P = (n) => new web3_js_1.PublicKey(Buffer.alloc(32, n));
432
432
  strict_1.default.equal(s.nonce, 9n);
433
433
  });
434
434
  });
435
+ (0, node_test_1.describe)("upstream close lifecycle builders", () => {
436
+ const PROGRAM = P(1);
437
+ (0, node_test_1.it)("requestClose carries disc 5 with payer signer + channel", () => {
438
+ const ix = (0, index_1.requestCloseIx)({
439
+ programId: PROGRAM,
440
+ payer: P(2),
441
+ channel: P(6),
442
+ });
443
+ strict_1.default.deepEqual(Array.from(ix.data), [5]);
444
+ strict_1.default.equal(ix.keys.length, 2);
445
+ strict_1.default.equal(ix.keys[0]?.isSigner, true);
446
+ });
447
+ (0, node_test_1.it)("seal carries disc 6 with channel only", () => {
448
+ const ix = (0, index_1.sealIx)({ programId: PROGRAM, channel: P(6) });
449
+ strict_1.default.deepEqual(Array.from(ix.data), [6]);
450
+ strict_1.default.equal(ix.keys.length, 1);
451
+ });
452
+ (0, node_test_1.it)("withdrawPayer carries disc 8 with 6 accounts", () => {
453
+ const ix = (0, index_1.withdrawPayerIx)({
454
+ programId: PROGRAM,
455
+ payer: P(2),
456
+ channel: P(6),
457
+ channelAta: P(8),
458
+ payerAta: P(7),
459
+ mint: P(4),
460
+ });
461
+ strict_1.default.deepEqual(Array.from(ix.data), [8]);
462
+ strict_1.default.equal(ix.keys.length, 6);
463
+ strict_1.default.equal(ix.keys[0]?.isSigner, true);
464
+ });
465
+ (0, node_test_1.it)("reclaim carries disc 9 with channel + rentPayer", () => {
466
+ const ix = (0, index_1.reclaimIx)({
467
+ programId: PROGRAM,
468
+ channel: P(6),
469
+ rentPayer: P(2),
470
+ });
471
+ strict_1.default.deepEqual(Array.from(ix.data), [9]);
472
+ strict_1.default.equal(ix.keys.length, 2);
473
+ });
474
+ });
475
+ (0, node_test_1.describe)("receipt expiry readers", () => {
476
+ const F = {
477
+ merchant: P(1),
478
+ binding: P(2),
479
+ cumulativeSpend: 10000n,
480
+ meterHash: new Uint8Array(32).fill(3),
481
+ outputHash: new Uint8Array(32).fill(4),
482
+ status: 0,
483
+ nonce: 7n,
484
+ expirySlot: 999n,
485
+ signer: P(1),
486
+ };
487
+ (0, node_test_1.it)("reads back the encoded expiry slot", () => {
488
+ const msg = (0, index_1.receiptMessageBytes)(F);
489
+ strict_1.default.equal((0, index_1.receiptExpirySlot)(msg), 999n);
490
+ });
491
+ (0, node_test_1.it)("expires strictly past the slot (mirrors onchain <= gate)", () => {
492
+ const msg = (0, index_1.receiptMessageBytes)(F);
493
+ strict_1.default.equal((0, index_1.isReceiptExpired)(msg, 998), false);
494
+ strict_1.default.equal((0, index_1.isReceiptExpired)(msg, 999), false);
495
+ strict_1.default.equal((0, index_1.isReceiptExpired)(msg, 1000), true);
496
+ });
497
+ });
498
+ (0, node_test_1.describe)("refundUnusedIx", () => {
499
+ (0, node_test_1.it)("carries the tabled discriminator with 6 accounts", () => {
500
+ const ix = (0, index_1.refundUnusedIx)(P(0), P(1), P(2), P(3), P(4), P(5));
501
+ strict_1.default.deepEqual(Array.from(ix.data), [239, 108, 1, 110, 2, 81, 44, 174]);
502
+ strict_1.default.equal(ix.keys.length, 6);
503
+ strict_1.default.equal(ix.keys[0]?.isSigner, true);
504
+ });
505
+ });
506
+ (0, node_test_1.describe)("distributeIx (empty plan)", () => {
507
+ (0, node_test_1.it)("encodes disc 7 + zero count with 11 fixed accounts, no signers", () => {
508
+ const ix = (0, index_1.distributeIx)({
509
+ programId: P(1),
510
+ channel: P(6),
511
+ payer: P(2),
512
+ rentPayer: P(2),
513
+ channelAta: P(8),
514
+ payerAta: P(7),
515
+ payeeAta: P(10),
516
+ treasuryAta: P(11),
517
+ mint: P(4),
518
+ eventAuthority: P(9),
519
+ });
520
+ strict_1.default.deepEqual(Array.from(ix.data), [7, 0, 0, 0, 0]);
521
+ strict_1.default.equal(ix.keys.length, 11);
522
+ strict_1.default.ok(ix.keys.every((k) => k.isSigner === false));
523
+ });
524
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@infantmen-labs/harbor-sdk",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "TypeScript SDK for Harbor bonded refunds on Solana: PDA helpers, instruction builders, receipt codec, ed25519 verify, account decoders.",
5
5
  "license": "MIT",
6
6
  "keywords": [