@infantmen-labs/harbor-sdk 0.4.4 → 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 +25 -0
- package/dist/src/accounts.d.ts +15 -0
- package/dist/src/accounts.js +17 -0
- package/dist/src/buyer.d.ts +61 -0
- package/dist/src/buyer.js +93 -0
- package/dist/src/channels.d.ts +90 -0
- package/dist/src/channels.js +166 -0
- package/dist/src/harbor-ix.d.ts +6 -0
- package/dist/src/harbor-ix.js +16 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +2 -0
- package/dist/src/pda.d.ts +4 -0
- package/dist/src/pda.js +11 -0
- package/dist/src/receipt.d.ts +14 -0
- package/dist/src/receipt.js +25 -1
- package/dist/test/vectors.test.js +310 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -31,3 +31,28 @@ What it covers: `bondPda` / `bindingPda` / `disputePda` / `receiptPda` /
|
|
|
31
31
|
entrypoints, the frozen 185-byte receipt layout, voucher bytes, and
|
|
32
32
|
zero-dependency account decoders. See `examples/bond-watch` for a
|
|
33
33
|
complete third-party monitor built on SDK + web3.js only.
|
|
34
|
+
|
|
35
|
+
## Buyer patterns (demand-side integration)
|
|
36
|
+
|
|
37
|
+
Upstream channel builders (`openChannelIx` / `topUpIx` / `settleIx`,
|
|
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:
|
|
43
|
+
|
|
44
|
+
1. **Gate on collateral.** Derive `bondPda(merchant, mint)` offline,
|
|
45
|
+
`decodeBond` it, refuse when `amount − reserved` is below your
|
|
46
|
+
policy — before opening any channel or locking any escrow.
|
|
47
|
+
2. **Cover every voucher.** `assertVoucherCoversQuote` enforces
|
|
48
|
+
voucher-delta ≥ quoted cost. This is billing-ack hygiene, NOT
|
|
49
|
+
delivery acceptance: pay for unit N+1 only after verifying unit N's
|
|
50
|
+
receipt AND accepting unit N's output bytes yourself. A valid
|
|
51
|
+
signature never proves correctness — nothing in this SDK will stop
|
|
52
|
+
you from treating it as one, so separate the two checks explicitly.
|
|
53
|
+
3. **Size disputes from chain, not memory.** `suggestClaimSpend`
|
|
54
|
+
returns `min(binding.maxSpend, funded ATA, floor(bondFree / 3))`.
|
|
55
|
+
A `0n` suggestion means no safe claim exists — do not open.
|
|
56
|
+
4. **Resolve via any live keeper.** Any keeper watching the program
|
|
57
|
+
resolves matured disputes; verify onchain (dispute account closed,
|
|
58
|
+
claimant +95%). Operate your own for production (see keeper README).
|
package/dist/src/accounts.d.ts
CHANGED
|
@@ -23,10 +23,25 @@ export interface Dispute {
|
|
|
23
23
|
stakeLamports: bigint;
|
|
24
24
|
claimSpend: bigint;
|
|
25
25
|
}
|
|
26
|
+
export interface Binding {
|
|
27
|
+
channel: PublicKey;
|
|
28
|
+
merchant: PublicKey;
|
|
29
|
+
bond: PublicKey;
|
|
30
|
+
channelProgram: PublicKey;
|
|
31
|
+
maxSpend: bigint;
|
|
32
|
+
lastNonce: bigint;
|
|
33
|
+
lastCumulativeSpend: bigint;
|
|
34
|
+
halted: boolean;
|
|
35
|
+
}
|
|
26
36
|
/** MerchantBond layout: disc(8) + merchant(32) + mint(32) + amount(8) +
|
|
27
37
|
* slaBps(2) + challengeSlots(8) + openDisputes(8) + lastChangeSlot(8) +
|
|
28
38
|
* reserved(8) + bump(1). */
|
|
29
39
|
export declare function decodeBond(data: Uint8Array): Bond;
|
|
40
|
+
/** ChannelBinding layout: disc(8) + channel(32) + merchant(32) +
|
|
41
|
+
* bond(32) + channel_program(32) + maxSpend(8) + lastNonce(8) +
|
|
42
|
+
* lastCumulativeSpend(8) + halted(1) + bump(1) = 162 bytes.
|
|
43
|
+
* Offsets verified against a live devnet binding. */
|
|
44
|
+
export declare function decodeBinding(data: Uint8Array): Binding;
|
|
30
45
|
/** Dispute layout: disc(8) + binding(32) + nonce(8) + reason(1) +
|
|
31
46
|
* claimant(32) + deadlineSlot(8) + stakeLamports(8) + claimSpend(8) +
|
|
32
47
|
* bump(1). */
|
package/dist/src/accounts.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.decodeBond = decodeBond;
|
|
4
|
+
exports.decodeBinding = decodeBinding;
|
|
4
5
|
exports.decodeDispute = decodeDispute;
|
|
5
6
|
const web3_js_1 = require("@solana/web3.js");
|
|
6
7
|
function u64(d, o) {
|
|
@@ -34,6 +35,22 @@ function decodeBond(data) {
|
|
|
34
35
|
reserved: u64(data, 106),
|
|
35
36
|
};
|
|
36
37
|
}
|
|
38
|
+
/** ChannelBinding layout: disc(8) + channel(32) + merchant(32) +
|
|
39
|
+
* bond(32) + channel_program(32) + maxSpend(8) + lastNonce(8) +
|
|
40
|
+
* lastCumulativeSpend(8) + halted(1) + bump(1) = 162 bytes.
|
|
41
|
+
* Offsets verified against a live devnet binding. */
|
|
42
|
+
function decodeBinding(data) {
|
|
43
|
+
return {
|
|
44
|
+
channel: pk(data, 8),
|
|
45
|
+
merchant: pk(data, 40),
|
|
46
|
+
bond: pk(data, 72),
|
|
47
|
+
channelProgram: pk(data, 104),
|
|
48
|
+
maxSpend: u64(data, 136),
|
|
49
|
+
lastNonce: u64(data, 144),
|
|
50
|
+
lastCumulativeSpend: u64(data, 152),
|
|
51
|
+
halted: data[160] !== 0,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
37
54
|
/** Dispute layout: disc(8) + binding(32) + nonce(8) + reason(1) +
|
|
38
55
|
* claimant(32) + deadlineSlot(8) + stakeLamports(8) + claimSpend(8) +
|
|
39
56
|
* bump(1). */
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Connection, PublicKey } from "@solana/web3.js";
|
|
2
|
+
/**
|
|
3
|
+
* Buyer-side incident helpers. These encode the operator judgments a
|
|
4
|
+
* demand buyer otherwise hand-rolls at 2am: how large a claim is safe,
|
|
5
|
+
* and whether a voucher covers the quoted price. Pure chain reads —
|
|
6
|
+
* nothing here opens disputes or moves funds.
|
|
7
|
+
*/
|
|
8
|
+
/** A voucher authorizing less than the quoted cost. */
|
|
9
|
+
export declare class VoucherUnderquoted extends Error {
|
|
10
|
+
readonly quoted: bigint;
|
|
11
|
+
readonly authorized: bigint;
|
|
12
|
+
constructor(quoted: bigint, authorized: bigint);
|
|
13
|
+
}
|
|
14
|
+
/** A voucher older than the channel watermark (replay or reorder). */
|
|
15
|
+
export declare class StaleVoucher extends Error {
|
|
16
|
+
readonly watermark: bigint;
|
|
17
|
+
readonly voucherCumulative: bigint;
|
|
18
|
+
constructor(watermark: bigint, voucherCumulative: bigint);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Coverage check: the voucher's marginal authorization must cover the
|
|
22
|
+
* quoted cost. This is billing-ack hygiene, NOT delivery acceptance —
|
|
23
|
+
* a covered voucher says the merchant may be paid, never that the
|
|
24
|
+
* output was correct (verify output bytes separately).
|
|
25
|
+
*/
|
|
26
|
+
export declare function assertVoucherCoversQuote(args: {
|
|
27
|
+
lastCumulative: bigint;
|
|
28
|
+
voucherCumulative: bigint;
|
|
29
|
+
quotedCost: bigint;
|
|
30
|
+
}): void;
|
|
31
|
+
export interface ClaimSuggestion {
|
|
32
|
+
/** Nonce to dispute (explicit, or binding.lastNonce + 1). */
|
|
33
|
+
nonce: bigint;
|
|
34
|
+
/** Onchain ceiling: claim_spend must be ≤ this (binding.maxSpend). */
|
|
35
|
+
maxSpend: bigint;
|
|
36
|
+
/** Claimant ATA balance for the bond mint. */
|
|
37
|
+
funded: bigint;
|
|
38
|
+
/** Bond free collateral (amount − reserved). */
|
|
39
|
+
bondFree: bigint;
|
|
40
|
+
/** Largest claim the bond can back: floor(free / 3). */
|
|
41
|
+
bondCap: bigint;
|
|
42
|
+
/**
|
|
43
|
+
* Suggested claim_spend = min(maxSpend, funded, bondCap). 0n means no
|
|
44
|
+
* safe claim exists — do NOT open a dispute (claim_spend must be > 0
|
|
45
|
+
* onchain); top up, wait, or walk away instead.
|
|
46
|
+
*/
|
|
47
|
+
claimSpend: bigint;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Maps "failed nonce N" to a safe claim amount. Reads the binding (cap +
|
|
51
|
+
* nonce default), the bond (free collateral — every unit of claim locks
|
|
52
|
+
* 3x: refund + fee + penalty), and the claimant's ATA (the claim stake
|
|
53
|
+
* must actually be funded). Throws when the binding or bond account is
|
|
54
|
+
* missing.
|
|
55
|
+
*/
|
|
56
|
+
export declare function suggestClaimSpend(conn: Connection, args: {
|
|
57
|
+
binding: PublicKey;
|
|
58
|
+
claimant: PublicKey;
|
|
59
|
+
mint: PublicKey;
|
|
60
|
+
nonce?: bigint;
|
|
61
|
+
}): Promise<ClaimSuggestion>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.StaleVoucher = exports.VoucherUnderquoted = void 0;
|
|
4
|
+
exports.assertVoucherCoversQuote = assertVoucherCoversQuote;
|
|
5
|
+
exports.suggestClaimSpend = suggestClaimSpend;
|
|
6
|
+
const accounts_1 = require("./accounts");
|
|
7
|
+
const pda_1 = require("./pda");
|
|
8
|
+
/**
|
|
9
|
+
* Buyer-side incident helpers. These encode the operator judgments a
|
|
10
|
+
* demand buyer otherwise hand-rolls at 2am: how large a claim is safe,
|
|
11
|
+
* and whether a voucher covers the quoted price. Pure chain reads —
|
|
12
|
+
* nothing here opens disputes or moves funds.
|
|
13
|
+
*/
|
|
14
|
+
/** A voucher authorizing less than the quoted cost. */
|
|
15
|
+
class VoucherUnderquoted extends Error {
|
|
16
|
+
quoted;
|
|
17
|
+
authorized;
|
|
18
|
+
constructor(quoted, authorized) {
|
|
19
|
+
super(`voucher authorizes ${authorized} but merchant quoted ${quoted}`);
|
|
20
|
+
this.quoted = quoted;
|
|
21
|
+
this.authorized = authorized;
|
|
22
|
+
this.name = "VoucherUnderquoted";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
exports.VoucherUnderquoted = VoucherUnderquoted;
|
|
26
|
+
/** A voucher older than the channel watermark (replay or reorder). */
|
|
27
|
+
class StaleVoucher extends Error {
|
|
28
|
+
watermark;
|
|
29
|
+
voucherCumulative;
|
|
30
|
+
constructor(watermark, voucherCumulative) {
|
|
31
|
+
super(`voucher cumulative ${voucherCumulative} is behind watermark ${watermark}`);
|
|
32
|
+
this.watermark = watermark;
|
|
33
|
+
this.voucherCumulative = voucherCumulative;
|
|
34
|
+
this.name = "StaleVoucher";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
exports.StaleVoucher = StaleVoucher;
|
|
38
|
+
/**
|
|
39
|
+
* Coverage check: the voucher's marginal authorization must cover the
|
|
40
|
+
* quoted cost. This is billing-ack hygiene, NOT delivery acceptance —
|
|
41
|
+
* a covered voucher says the merchant may be paid, never that the
|
|
42
|
+
* output was correct (verify output bytes separately).
|
|
43
|
+
*/
|
|
44
|
+
function assertVoucherCoversQuote(args) {
|
|
45
|
+
if (args.voucherCumulative < args.lastCumulative) {
|
|
46
|
+
throw new StaleVoucher(args.lastCumulative, args.voucherCumulative);
|
|
47
|
+
}
|
|
48
|
+
const authorized = args.voucherCumulative - args.lastCumulative;
|
|
49
|
+
if (authorized < args.quotedCost) {
|
|
50
|
+
throw new VoucherUnderquoted(args.quotedCost, authorized);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Maps "failed nonce N" to a safe claim amount. Reads the binding (cap +
|
|
55
|
+
* nonce default), the bond (free collateral — every unit of claim locks
|
|
56
|
+
* 3x: refund + fee + penalty), and the claimant's ATA (the claim stake
|
|
57
|
+
* must actually be funded). Throws when the binding or bond account is
|
|
58
|
+
* missing.
|
|
59
|
+
*/
|
|
60
|
+
async function suggestClaimSpend(conn, args) {
|
|
61
|
+
const bindingInfo = await conn.getAccountInfo(args.binding);
|
|
62
|
+
if (bindingInfo === null)
|
|
63
|
+
throw new Error("binding account missing");
|
|
64
|
+
const binding = (0, accounts_1.decodeBinding)(bindingInfo.data);
|
|
65
|
+
const nonce = args.nonce ?? binding.lastNonce + 1n;
|
|
66
|
+
const bondInfo = await conn.getAccountInfo(binding.bond);
|
|
67
|
+
if (bondInfo === null)
|
|
68
|
+
throw new Error("bond account missing");
|
|
69
|
+
const bond = (0, accounts_1.decodeBond)(bondInfo.data);
|
|
70
|
+
const bondFree = bond.amount - bond.reserved;
|
|
71
|
+
// Outflow per unit of claim is 3x (refund + fee + penalty).
|
|
72
|
+
const bondCap = bondFree >= 0n ? bondFree / 3n : 0n;
|
|
73
|
+
const claimantAta = (0, pda_1.ataFor)(args.claimant, args.mint);
|
|
74
|
+
const ataInfo = await conn.getAccountInfo(claimantAta);
|
|
75
|
+
let funded = 0n;
|
|
76
|
+
if (ataInfo !== null) {
|
|
77
|
+
const bal = await conn.getTokenAccountBalance(claimantAta);
|
|
78
|
+
funded = BigInt(bal.value.amount);
|
|
79
|
+
}
|
|
80
|
+
let claimSpend = binding.maxSpend;
|
|
81
|
+
if (funded < claimSpend)
|
|
82
|
+
claimSpend = funded;
|
|
83
|
+
if (bondCap < claimSpend)
|
|
84
|
+
claimSpend = bondCap;
|
|
85
|
+
return {
|
|
86
|
+
nonce,
|
|
87
|
+
maxSpend: binding.maxSpend,
|
|
88
|
+
funded,
|
|
89
|
+
bondFree,
|
|
90
|
+
bondCap,
|
|
91
|
+
claimSpend,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { PublicKey, TransactionInstruction } from "@solana/web3.js";
|
|
2
|
+
/** Upstream `open`. Layout: disc(1) | salt(8) | deposit(8) | grace(4) | slot(8) | recipients u32. */
|
|
3
|
+
export declare function openChannelIx(args: {
|
|
4
|
+
programId: PublicKey;
|
|
5
|
+
payer: PublicKey;
|
|
6
|
+
payee: PublicKey;
|
|
7
|
+
mint: PublicKey;
|
|
8
|
+
authorizedSigner: PublicKey;
|
|
9
|
+
channel: PublicKey;
|
|
10
|
+
payerAta: PublicKey;
|
|
11
|
+
channelAta: PublicKey;
|
|
12
|
+
eventAuthority: PublicKey;
|
|
13
|
+
salt: bigint;
|
|
14
|
+
deposit: bigint;
|
|
15
|
+
gracePeriod: number;
|
|
16
|
+
openSlot: bigint;
|
|
17
|
+
}): TransactionInstruction;
|
|
18
|
+
export declare function settleIx(programId: PublicKey, channel: PublicKey): TransactionInstruction;
|
|
19
|
+
/** Upstream `top_up`. Layout: disc(3) | amount(8). */
|
|
20
|
+
export declare function topUpIx(args: {
|
|
21
|
+
programId: PublicKey;
|
|
22
|
+
payer: PublicKey;
|
|
23
|
+
channel: PublicKey;
|
|
24
|
+
payerAta: PublicKey;
|
|
25
|
+
channelAta: PublicKey;
|
|
26
|
+
mint: PublicKey;
|
|
27
|
+
amount: bigint;
|
|
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;
|
|
88
|
+
export declare function deriveChannel(programId: PublicKey, payer: PublicKey, payee: PublicKey, mint: PublicKey, authorizedSigner: PublicKey, salt: bigint, openSlot: bigint): {
|
|
89
|
+
channel: PublicKey;
|
|
90
|
+
};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.openChannelIx = openChannelIx;
|
|
4
|
+
exports.settleIx = settleIx;
|
|
5
|
+
exports.topUpIx = topUpIx;
|
|
6
|
+
exports.requestCloseIx = requestCloseIx;
|
|
7
|
+
exports.sealIx = sealIx;
|
|
8
|
+
exports.withdrawPayerIx = withdrawPayerIx;
|
|
9
|
+
exports.reclaimIx = reclaimIx;
|
|
10
|
+
exports.distributeIx = distributeIx;
|
|
11
|
+
exports.deriveChannel = deriveChannel;
|
|
12
|
+
const web3_js_1 = require("@solana/web3.js");
|
|
13
|
+
const ids_1 = require("./ids");
|
|
14
|
+
const pda_1 = require("./pda");
|
|
15
|
+
const u64_1 = require("./u64");
|
|
16
|
+
/**
|
|
17
|
+
* Upstream payment-channels instruction builders. These encode the pinned
|
|
18
|
+
* upstream layouts (see docs/upstream-pin.md) so third-party buyers never
|
|
19
|
+
* reimplement consensus-critical bytes by copy-paste. Previously lived in
|
|
20
|
+
* the reference agent; moved here verbatim (byte-identical output).
|
|
21
|
+
*/
|
|
22
|
+
function m(pubkey, writable, signer) {
|
|
23
|
+
return { pubkey, isWritable: writable, isSigner: signer };
|
|
24
|
+
}
|
|
25
|
+
function u32(v) {
|
|
26
|
+
const b = Buffer.alloc(4);
|
|
27
|
+
b.writeUInt32LE(v);
|
|
28
|
+
return b;
|
|
29
|
+
}
|
|
30
|
+
/** Upstream `open`. Layout: disc(1) | salt(8) | deposit(8) | grace(4) | slot(8) | recipients u32. */
|
|
31
|
+
function openChannelIx(args) {
|
|
32
|
+
const data = Buffer.concat([
|
|
33
|
+
Buffer.from([1]),
|
|
34
|
+
(0, u64_1.u64le)(args.salt),
|
|
35
|
+
(0, u64_1.u64le)(args.deposit),
|
|
36
|
+
u32(args.gracePeriod),
|
|
37
|
+
(0, u64_1.u64le)(args.openSlot),
|
|
38
|
+
u32(0),
|
|
39
|
+
]);
|
|
40
|
+
return new web3_js_1.TransactionInstruction({
|
|
41
|
+
programId: args.programId,
|
|
42
|
+
keys: [
|
|
43
|
+
m(args.payer, true, true),
|
|
44
|
+
m(args.payer, true, true),
|
|
45
|
+
m(args.payee, false, false),
|
|
46
|
+
m(args.mint, false, false),
|
|
47
|
+
m(args.authorizedSigner, false, false),
|
|
48
|
+
m(args.channel, true, false),
|
|
49
|
+
m(args.payerAta, true, false),
|
|
50
|
+
m(args.channelAta, true, false),
|
|
51
|
+
m(ids_1.TOKEN_PROGRAM_ID, false, false),
|
|
52
|
+
m(ids_1.SYSTEM_PROGRAM_ID, false, false),
|
|
53
|
+
m(ids_1.RENT_SYSVAR_ID, false, false),
|
|
54
|
+
m(ids_1.ATA_PROGRAM_ID, false, false),
|
|
55
|
+
m(args.eventAuthority, false, false),
|
|
56
|
+
m(args.programId, false, false),
|
|
57
|
+
],
|
|
58
|
+
data,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function settleIx(programId, channel) {
|
|
62
|
+
return new web3_js_1.TransactionInstruction({
|
|
63
|
+
programId,
|
|
64
|
+
keys: [m(channel, true, false), m(ids_1.IX_SYSVAR_ID, false, false)],
|
|
65
|
+
data: Buffer.from([2]),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/** Upstream `top_up`. Layout: disc(3) | amount(8). */
|
|
69
|
+
function topUpIx(args) {
|
|
70
|
+
return new web3_js_1.TransactionInstruction({
|
|
71
|
+
programId: args.programId,
|
|
72
|
+
keys: [
|
|
73
|
+
m(args.payer, true, true),
|
|
74
|
+
m(args.channel, true, false),
|
|
75
|
+
m(args.payerAta, true, false),
|
|
76
|
+
m(args.channelAta, true, false),
|
|
77
|
+
m(args.mint, false, false),
|
|
78
|
+
m(ids_1.TOKEN_PROGRAM_ID, false, false),
|
|
79
|
+
],
|
|
80
|
+
data: Buffer.concat([Buffer.from([3]), (0, u64_1.u64le)(args.amount)]),
|
|
81
|
+
});
|
|
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
|
+
}
|
|
163
|
+
function deriveChannel(programId, payer, payee, mint, authorizedSigner, salt, openSlot) {
|
|
164
|
+
const [channel] = (0, pda_1.channelPda)(programId, payer, payee, mint, authorizedSigner, salt, openSlot);
|
|
165
|
+
return { channel };
|
|
166
|
+
}
|
package/dist/src/harbor-ix.d.ts
CHANGED
|
@@ -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;
|
package/dist/src/harbor-ix.js
CHANGED
|
@@ -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/index.d.ts
CHANGED
package/dist/src/index.js
CHANGED
|
@@ -20,6 +20,8 @@ __exportStar(require("./accounts"), exports);
|
|
|
20
20
|
__exportStar(require("./receipt"), exports);
|
|
21
21
|
__exportStar(require("./tx"), exports);
|
|
22
22
|
__exportStar(require("./u64"), exports);
|
|
23
|
+
__exportStar(require("./channels"), exports);
|
|
24
|
+
__exportStar(require("./buyer"), exports);
|
|
23
25
|
__exportStar(require("./ed25519"), exports);
|
|
24
26
|
__exportStar(require("./verify"), exports);
|
|
25
27
|
__exportStar(require("./harbor-ix"), exports);
|
package/dist/src/pda.d.ts
CHANGED
|
@@ -7,4 +7,8 @@ export declare function treasuryPda(mint: PublicKey): [PublicKey, number];
|
|
|
7
7
|
/** Tombstone marking a (binding, nonce) as claimed exactly once. */
|
|
8
8
|
export declare function claimPda(binding: PublicKey, nonce: bigint): [PublicKey, number];
|
|
9
9
|
export declare function channelPda(channelProgram: PublicKey, payer: PublicKey, payee: PublicKey, mint: PublicKey, authorizedSigner: PublicKey, salt: bigint, openSlot: bigint): [PublicKey, number];
|
|
10
|
+
/** Canonical associated token address for (owner, mint). */
|
|
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;
|
|
10
14
|
export declare function channelAta(channel: PublicKey, tokenProgram: PublicKey, mint: PublicKey, ataProgram: PublicKey): [PublicKey, number];
|
package/dist/src/pda.js
CHANGED
|
@@ -7,9 +7,12 @@ exports.disputePda = disputePda;
|
|
|
7
7
|
exports.treasuryPda = treasuryPda;
|
|
8
8
|
exports.claimPda = claimPda;
|
|
9
9
|
exports.channelPda = channelPda;
|
|
10
|
+
exports.ataFor = ataFor;
|
|
11
|
+
exports.vaultAta = vaultAta;
|
|
10
12
|
exports.channelAta = channelAta;
|
|
11
13
|
const web3_js_1 = require("@solana/web3.js");
|
|
12
14
|
const ids_1 = require("./ids");
|
|
15
|
+
const ids_2 = require("./ids");
|
|
13
16
|
const u64_1 = require("./u64");
|
|
14
17
|
function bondPda(merchant, mint) {
|
|
15
18
|
return web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("bond"), merchant.toBuffer(), mint.toBuffer()], ids_1.HARBOR_PROGRAM_ID);
|
|
@@ -46,6 +49,14 @@ function channelPda(channelProgram, payer, payee, mint, authorizedSigner, salt,
|
|
|
46
49
|
slotBuf,
|
|
47
50
|
], channelProgram);
|
|
48
51
|
}
|
|
52
|
+
/** Canonical associated token address for (owner, mint). */
|
|
53
|
+
function ataFor(owner, mint) {
|
|
54
|
+
return web3_js_1.PublicKey.findProgramAddressSync([owner.toBuffer(), ids_2.TOKEN_PROGRAM_ID.toBuffer(), mint.toBuffer()], ids_2.ATA_PROGRAM_ID)[0];
|
|
55
|
+
}
|
|
56
|
+
/** Bond vault: the canonical ATA of the bond PDA (holds collateral). */
|
|
57
|
+
function vaultAta(bond, mint) {
|
|
58
|
+
return ataFor(bond, mint);
|
|
59
|
+
}
|
|
49
60
|
function channelAta(channel, tokenProgram, mint, ataProgram) {
|
|
50
61
|
return web3_js_1.PublicKey.findProgramAddressSync([channel.toBuffer(), tokenProgram.toBuffer(), mint.toBuffer()], ataProgram);
|
|
51
62
|
}
|
package/dist/src/receipt.d.ts
CHANGED
|
@@ -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;
|
package/dist/src/receipt.js
CHANGED
|
@@ -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
|
+
}
|
|
@@ -212,3 +212,313 @@ const P = (n) => new web3_js_1.PublicKey(Buffer.alloc(32, n));
|
|
|
212
212
|
strict_1.default.throws(() => (0, index_1.writeI64LE)(Buffer.alloc(8), 2n ** 63n, 0), RangeError);
|
|
213
213
|
});
|
|
214
214
|
});
|
|
215
|
+
(0, node_test_1.describe)("upstream channel builders (moved from agent, byte-identical)", () => {
|
|
216
|
+
const PROGRAM = P(1);
|
|
217
|
+
const args = {
|
|
218
|
+
programId: PROGRAM,
|
|
219
|
+
payer: P(2),
|
|
220
|
+
payee: P(3),
|
|
221
|
+
mint: P(4),
|
|
222
|
+
authorizedSigner: P(5),
|
|
223
|
+
channel: P(6),
|
|
224
|
+
payerAta: P(7),
|
|
225
|
+
channelAta: P(8),
|
|
226
|
+
eventAuthority: P(9),
|
|
227
|
+
salt: 42n,
|
|
228
|
+
deposit: 5000000n,
|
|
229
|
+
gracePeriod: 7200,
|
|
230
|
+
openSlot: 99n,
|
|
231
|
+
};
|
|
232
|
+
(0, node_test_1.it)("open encodes disc | salt | deposit | grace | slot | recipients", () => {
|
|
233
|
+
const ix = (0, index_1.openChannelIx)(args);
|
|
234
|
+
const d = Buffer.from(ix.data);
|
|
235
|
+
strict_1.default.equal(d.readUInt8(0), 1);
|
|
236
|
+
strict_1.default.equal(d.readBigUInt64LE(1), 42n);
|
|
237
|
+
strict_1.default.equal(d.readBigUInt64LE(9), 5000000n);
|
|
238
|
+
strict_1.default.equal(d.readUInt32LE(17), 7200);
|
|
239
|
+
strict_1.default.equal(d.readBigUInt64LE(21), 99n);
|
|
240
|
+
strict_1.default.equal(d.readUInt32LE(29), 0);
|
|
241
|
+
strict_1.default.equal(d.length, 33);
|
|
242
|
+
strict_1.default.equal(ix.keys.length, 14);
|
|
243
|
+
strict_1.default.ok(ix.programId.equals(PROGRAM));
|
|
244
|
+
});
|
|
245
|
+
(0, node_test_1.it)("settle is a bare discriminator with channel + sysvar", () => {
|
|
246
|
+
const ix = (0, index_1.settleIx)(PROGRAM, P(6));
|
|
247
|
+
strict_1.default.deepEqual(Array.from(ix.data), [2]);
|
|
248
|
+
strict_1.default.equal(ix.keys.length, 2);
|
|
249
|
+
});
|
|
250
|
+
(0, node_test_1.it)("top_up encodes disc | amount with 6 accounts", () => {
|
|
251
|
+
const ix = (0, index_1.topUpIx)({
|
|
252
|
+
programId: PROGRAM,
|
|
253
|
+
payer: P(2),
|
|
254
|
+
channel: P(6),
|
|
255
|
+
payerAta: P(7),
|
|
256
|
+
channelAta: P(8),
|
|
257
|
+
mint: P(4),
|
|
258
|
+
amount: 9n,
|
|
259
|
+
});
|
|
260
|
+
const d = Buffer.from(ix.data);
|
|
261
|
+
strict_1.default.equal(d.readUInt8(0), 3);
|
|
262
|
+
strict_1.default.equal(d.readBigUInt64LE(1), 9n);
|
|
263
|
+
strict_1.default.equal(d.length, 9);
|
|
264
|
+
strict_1.default.equal(ix.keys.length, 6);
|
|
265
|
+
});
|
|
266
|
+
(0, node_test_1.it)("deriveChannel matches channelPda", () => {
|
|
267
|
+
const { channel } = (0, index_1.deriveChannel)(P(1), P(2), P(3), P(4), P(5), 42n, 99n);
|
|
268
|
+
strict_1.default.equal(channel.toBase58().length, 44);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
(0, node_test_1.describe)("decodeBinding", () => {
|
|
272
|
+
function bindingBytes() {
|
|
273
|
+
const b = Buffer.alloc(162);
|
|
274
|
+
P(6).toBuffer().copy(b, 8);
|
|
275
|
+
P(11).toBuffer().copy(b, 40);
|
|
276
|
+
P(12).toBuffer().copy(b, 72);
|
|
277
|
+
P(13).toBuffer().copy(b, 104);
|
|
278
|
+
b.writeBigUInt64LE(2000n, 136);
|
|
279
|
+
b.writeBigUInt64LE(4n, 144);
|
|
280
|
+
b.writeBigUInt64LE(15000n, 152);
|
|
281
|
+
b.writeUInt8(0, 160);
|
|
282
|
+
return b;
|
|
283
|
+
}
|
|
284
|
+
(0, node_test_1.it)("decodes every field at its documented offset", () => {
|
|
285
|
+
const d = (0, index_1.decodeBinding)(bindingBytes());
|
|
286
|
+
strict_1.default.ok(d.channel.equals(P(6)));
|
|
287
|
+
strict_1.default.ok(d.merchant.equals(P(11)));
|
|
288
|
+
strict_1.default.ok(d.bond.equals(P(12)));
|
|
289
|
+
strict_1.default.ok(d.channelProgram.equals(P(13)));
|
|
290
|
+
strict_1.default.equal(d.maxSpend, 2000n);
|
|
291
|
+
strict_1.default.equal(d.lastNonce, 4n);
|
|
292
|
+
strict_1.default.equal(d.lastCumulativeSpend, 15000n);
|
|
293
|
+
strict_1.default.equal(d.halted, false);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
(0, node_test_1.describe)("assertVoucherCoversQuote", () => {
|
|
297
|
+
(0, node_test_1.it)("passes when the marginal authorization covers the quote", () => {
|
|
298
|
+
(0, index_1.assertVoucherCoversQuote)({
|
|
299
|
+
lastCumulative: 10000n,
|
|
300
|
+
voucherCumulative: 12050n,
|
|
301
|
+
quotedCost: 2000n,
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
(0, node_test_1.it)("throws VoucherUnderquoted on short authorization", () => {
|
|
305
|
+
strict_1.default.throws(() => (0, index_1.assertVoucherCoversQuote)({
|
|
306
|
+
lastCumulative: 10000n,
|
|
307
|
+
voucherCumulative: 11000n,
|
|
308
|
+
quotedCost: 2000n,
|
|
309
|
+
}), index_1.VoucherUnderquoted);
|
|
310
|
+
});
|
|
311
|
+
(0, node_test_1.it)("throws StaleVoucher when behind the watermark", () => {
|
|
312
|
+
strict_1.default.throws(() => (0, index_1.assertVoucherCoversQuote)({
|
|
313
|
+
lastCumulative: 10000n,
|
|
314
|
+
voucherCumulative: 9000n,
|
|
315
|
+
quotedCost: 1n,
|
|
316
|
+
}), index_1.StaleVoucher);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
(0, node_test_1.describe)("suggestClaimSpend", () => {
|
|
320
|
+
const BINDING = P(20);
|
|
321
|
+
const CLAIMANT = P(21);
|
|
322
|
+
const MINT = P(22);
|
|
323
|
+
const BOND = P(23);
|
|
324
|
+
function bondBytes(amount, reserved) {
|
|
325
|
+
const b = Buffer.alloc(115);
|
|
326
|
+
P(11).toBuffer().copy(b, 8);
|
|
327
|
+
MINT.toBuffer().copy(b, 40);
|
|
328
|
+
b.writeBigUInt64LE(amount, 72);
|
|
329
|
+
b.writeBigUInt64LE(reserved, 106);
|
|
330
|
+
return b;
|
|
331
|
+
}
|
|
332
|
+
function bindingBytes(maxSpend, lastNonce) {
|
|
333
|
+
const b = Buffer.alloc(162);
|
|
334
|
+
P(6).toBuffer().copy(b, 8);
|
|
335
|
+
P(11).toBuffer().copy(b, 40);
|
|
336
|
+
BOND.toBuffer().copy(b, 72);
|
|
337
|
+
P(13).toBuffer().copy(b, 104);
|
|
338
|
+
b.writeBigUInt64LE(maxSpend, 136);
|
|
339
|
+
b.writeBigUInt64LE(lastNonce, 144);
|
|
340
|
+
return b;
|
|
341
|
+
}
|
|
342
|
+
function mockConn(opts) {
|
|
343
|
+
const ata = (0, index_1.ataFor)(CLAIMANT, MINT);
|
|
344
|
+
return {
|
|
345
|
+
getAccountInfo: async (k) => {
|
|
346
|
+
const key = k.toBase58();
|
|
347
|
+
if (key === BINDING.toBase58())
|
|
348
|
+
return opts.binding === null ? null : { data: opts.binding };
|
|
349
|
+
if (key === BOND.toBase58())
|
|
350
|
+
return { data: bondBytes(opts.bondAmount, opts.bondReserved) };
|
|
351
|
+
if (key === ata.toBase58())
|
|
352
|
+
return opts.ataExists ? { data: Buffer.alloc(165) } : null;
|
|
353
|
+
return null;
|
|
354
|
+
},
|
|
355
|
+
getTokenAccountBalance: async () => ({
|
|
356
|
+
value: { amount: opts.funded.toString() },
|
|
357
|
+
}),
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
const base = {
|
|
361
|
+
binding: BINDING,
|
|
362
|
+
claimant: CLAIMANT,
|
|
363
|
+
mint: MINT,
|
|
364
|
+
};
|
|
365
|
+
(0, node_test_1.it)("caps at binding maxSpend and defaults nonce to lastNonce + 1", async () => {
|
|
366
|
+
const conn = mockConn({
|
|
367
|
+
binding: bindingBytes(2000n, 4n),
|
|
368
|
+
bondAmount: 500000n,
|
|
369
|
+
bondReserved: 0n,
|
|
370
|
+
funded: 100000n,
|
|
371
|
+
ataExists: true,
|
|
372
|
+
});
|
|
373
|
+
const s = await (0, index_1.suggestClaimSpend)(conn, base);
|
|
374
|
+
strict_1.default.equal(s.nonce, 5n);
|
|
375
|
+
strict_1.default.equal(s.maxSpend, 2000n);
|
|
376
|
+
strict_1.default.equal(s.funded, 100000n);
|
|
377
|
+
strict_1.default.equal(s.claimSpend, 2000n);
|
|
378
|
+
});
|
|
379
|
+
(0, node_test_1.it)("caps at funded balance when the claimant is thin", async () => {
|
|
380
|
+
const conn = mockConn({
|
|
381
|
+
binding: bindingBytes(2000n, 4n),
|
|
382
|
+
bondAmount: 500000n,
|
|
383
|
+
bondReserved: 0n,
|
|
384
|
+
funded: 500n,
|
|
385
|
+
ataExists: true,
|
|
386
|
+
});
|
|
387
|
+
const s = await (0, index_1.suggestClaimSpend)(conn, base);
|
|
388
|
+
strict_1.default.equal(s.claimSpend, 500n);
|
|
389
|
+
});
|
|
390
|
+
(0, node_test_1.it)("caps at bond capacity floor(free / 3)", async () => {
|
|
391
|
+
const conn = mockConn({
|
|
392
|
+
binding: bindingBytes(2000n, 4n),
|
|
393
|
+
bondAmount: 3000n,
|
|
394
|
+
bondReserved: 0n,
|
|
395
|
+
funded: 100000n,
|
|
396
|
+
ataExists: true,
|
|
397
|
+
});
|
|
398
|
+
const s = await (0, index_1.suggestClaimSpend)(conn, base);
|
|
399
|
+
strict_1.default.equal(s.bondFree, 3000n);
|
|
400
|
+
strict_1.default.equal(s.bondCap, 1000n);
|
|
401
|
+
strict_1.default.equal(s.claimSpend, 1000n);
|
|
402
|
+
});
|
|
403
|
+
(0, node_test_1.it)("suggests 0 when the ATA is missing (do not open)", async () => {
|
|
404
|
+
const conn = mockConn({
|
|
405
|
+
binding: bindingBytes(2000n, 4n),
|
|
406
|
+
bondAmount: 500000n,
|
|
407
|
+
bondReserved: 0n,
|
|
408
|
+
funded: 0n,
|
|
409
|
+
ataExists: false,
|
|
410
|
+
});
|
|
411
|
+
const s = await (0, index_1.suggestClaimSpend)(conn, base);
|
|
412
|
+
strict_1.default.equal(s.funded, 0n);
|
|
413
|
+
strict_1.default.equal(s.claimSpend, 0n);
|
|
414
|
+
});
|
|
415
|
+
(0, node_test_1.it)("throws on missing binding and passes explicit nonce", async () => {
|
|
416
|
+
const conn = mockConn({
|
|
417
|
+
binding: null,
|
|
418
|
+
bondAmount: 0n,
|
|
419
|
+
bondReserved: 0n,
|
|
420
|
+
funded: 0n,
|
|
421
|
+
ataExists: false,
|
|
422
|
+
});
|
|
423
|
+
await strict_1.default.rejects((0, index_1.suggestClaimSpend)(conn, base), /binding/);
|
|
424
|
+
const conn2 = mockConn({
|
|
425
|
+
binding: bindingBytes(2000n, 4n),
|
|
426
|
+
bondAmount: 500000n,
|
|
427
|
+
bondReserved: 0n,
|
|
428
|
+
funded: 100000n,
|
|
429
|
+
ataExists: true,
|
|
430
|
+
});
|
|
431
|
+
const s = await (0, index_1.suggestClaimSpend)(conn2, { ...base, nonce: 9n });
|
|
432
|
+
strict_1.default.equal(s.nonce, 9n);
|
|
433
|
+
});
|
|
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.
|
|
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": [
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"types": "dist/src/index.d.ts",
|
|
18
18
|
"scripts": {
|
|
19
19
|
"build": "tsc -p .",
|
|
20
|
+
"clean": "rm -rf dist",
|
|
20
21
|
"test": "node --test dist/test/vectors.test.js"
|
|
21
22
|
},
|
|
22
23
|
"dependencies": {
|