@infantmen-labs/harbor-sdk 0.4.1 → 0.4.4
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 +33 -0
- package/dist/src/ed25519.js +3 -2
- package/dist/src/harbor-ix.js +2 -3
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +2 -0
- package/dist/src/pda.js +6 -10
- package/dist/src/receipt.js +4 -3
- package/dist/src/tx.d.ts +11 -0
- package/dist/src/tx.js +32 -0
- package/dist/src/u64.d.ts +11 -0
- package/dist/src/u64.js +35 -0
- package/dist/test/vectors.test.js +71 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# harbor-sdk
|
|
2
|
+
|
|
3
|
+
TypeScript SDK for Harbor bonded refunds on Solana: PDA helpers,
|
|
4
|
+
instruction builders, receipt codec, ed25519 verify, account decoders.
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
npm i @infantmen-labs/harbor-sdk
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import {
|
|
12
|
+
bondPda,
|
|
13
|
+
decodeBond,
|
|
14
|
+
receiptMessageBytes,
|
|
15
|
+
verifyEd25519,
|
|
16
|
+
} from "@infantmen-labs/harbor-sdk";
|
|
17
|
+
|
|
18
|
+
// Locate any merchant's bond — derived offline, no RPC call.
|
|
19
|
+
const [bond] = bondPda(merchant, mint);
|
|
20
|
+
|
|
21
|
+
// Read its health: bonded, reserved, and free collateral.
|
|
22
|
+
const info = await connection.getAccountInfo(bond);
|
|
23
|
+
const { amount, reserved, openDisputes } = decodeBond(info.data);
|
|
24
|
+
|
|
25
|
+
// Verify every delivery before paying for the next unit.
|
|
26
|
+
const ok = verifyEd25519(signer, receiptMessageBytes(receipt), signature);
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
What it covers: `bondPda` / `bindingPda` / `disputePda` / `receiptPda` /
|
|
30
|
+
`treasuryPda` / `claimPda`, instruction builders for all 11 program
|
|
31
|
+
entrypoints, the frozen 185-byte receipt layout, voucher bytes, and
|
|
32
|
+
zero-dependency account decoders. See `examples/bond-watch` for a
|
|
33
|
+
complete third-party monitor built on SDK + web3.js only.
|
package/dist/src/ed25519.js
CHANGED
|
@@ -5,6 +5,7 @@ exports.buildEd25519Ix = buildEd25519Ix;
|
|
|
5
5
|
exports.channelVoucherBytes = channelVoucherBytes;
|
|
6
6
|
const web3_js_1 = require("@solana/web3.js");
|
|
7
7
|
const ids_1 = require("./ids");
|
|
8
|
+
const u64_1 = require("./u64");
|
|
8
9
|
/**
|
|
9
10
|
* Canonical single-signature Ed25519 precompile ix. Offsets point into this
|
|
10
11
|
* ix's own data (indices u16::MAX). Layout: header(16) | pubkey(32) |
|
|
@@ -40,8 +41,8 @@ function channelVoucherBytes(channel, cumulative, expiresAt) {
|
|
|
40
41
|
out.writeUInt8(0x56, 0);
|
|
41
42
|
out.writeUInt8(0x01, 1);
|
|
42
43
|
channel.toBuffer().copy(out, 2);
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
(0, u64_1.writeU64LE)(out, cumulative, 34);
|
|
45
|
+
(0, u64_1.writeI64LE)(out, expiresAt, 42);
|
|
45
46
|
return out;
|
|
46
47
|
}
|
|
47
48
|
exports.VOUCHER_LEN = 50;
|
package/dist/src/harbor-ix.js
CHANGED
|
@@ -12,6 +12,7 @@ exports.haltBindingIx = haltBindingIx;
|
|
|
12
12
|
exports.resolveTimeoutIx = resolveTimeoutIx;
|
|
13
13
|
const web3_js_1 = require("@solana/web3.js");
|
|
14
14
|
const ids_1 = require("./ids");
|
|
15
|
+
const u64_1 = require("./u64");
|
|
15
16
|
const D = {
|
|
16
17
|
registerMerchant: [238, 245, 77, 132, 161, 88, 216, 248],
|
|
17
18
|
postBond: [168, 151, 202, 119, 163, 58, 147, 247],
|
|
@@ -36,9 +37,7 @@ function u16(v) {
|
|
|
36
37
|
return b;
|
|
37
38
|
}
|
|
38
39
|
function u64(v) {
|
|
39
|
-
|
|
40
|
-
b.writeBigUInt64LE(v);
|
|
41
|
-
return b;
|
|
40
|
+
return (0, u64_1.u64le)(v);
|
|
42
41
|
}
|
|
43
42
|
function pk(k) {
|
|
44
43
|
return k.toBuffer();
|
package/dist/src/index.d.ts
CHANGED
package/dist/src/index.js
CHANGED
|
@@ -18,6 +18,8 @@ __exportStar(require("./ids"), exports);
|
|
|
18
18
|
__exportStar(require("./pda"), exports);
|
|
19
19
|
__exportStar(require("./accounts"), exports);
|
|
20
20
|
__exportStar(require("./receipt"), exports);
|
|
21
|
+
__exportStar(require("./tx"), exports);
|
|
22
|
+
__exportStar(require("./u64"), exports);
|
|
21
23
|
__exportStar(require("./ed25519"), exports);
|
|
22
24
|
__exportStar(require("./verify"), exports);
|
|
23
25
|
__exportStar(require("./harbor-ix"), exports);
|
package/dist/src/pda.js
CHANGED
|
@@ -10,6 +10,7 @@ exports.channelPda = channelPda;
|
|
|
10
10
|
exports.channelAta = channelAta;
|
|
11
11
|
const web3_js_1 = require("@solana/web3.js");
|
|
12
12
|
const ids_1 = require("./ids");
|
|
13
|
+
const u64_1 = require("./u64");
|
|
13
14
|
function bondPda(merchant, mint) {
|
|
14
15
|
return web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("bond"), merchant.toBuffer(), mint.toBuffer()], ids_1.HARBOR_PROGRAM_ID);
|
|
15
16
|
}
|
|
@@ -17,13 +18,11 @@ function bindingPda(channel) {
|
|
|
17
18
|
return web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("binding"), channel.toBuffer()], ids_1.HARBOR_PROGRAM_ID);
|
|
18
19
|
}
|
|
19
20
|
function receiptPda(binding, nonce) {
|
|
20
|
-
const nonceBuf =
|
|
21
|
-
nonceBuf.writeBigUInt64LE(nonce);
|
|
21
|
+
const nonceBuf = (0, u64_1.u64le)(nonce);
|
|
22
22
|
return web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("receipt"), binding.toBuffer(), nonceBuf], ids_1.HARBOR_PROGRAM_ID);
|
|
23
23
|
}
|
|
24
24
|
function disputePda(binding, nonce) {
|
|
25
|
-
const nonceBuf =
|
|
26
|
-
nonceBuf.writeBigUInt64LE(nonce);
|
|
25
|
+
const nonceBuf = (0, u64_1.u64le)(nonce);
|
|
27
26
|
return web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("dispute"), binding.toBuffer(), nonceBuf], ids_1.HARBOR_PROGRAM_ID);
|
|
28
27
|
}
|
|
29
28
|
function treasuryPda(mint) {
|
|
@@ -31,15 +30,12 @@ function treasuryPda(mint) {
|
|
|
31
30
|
}
|
|
32
31
|
/** Tombstone marking a (binding, nonce) as claimed exactly once. */
|
|
33
32
|
function claimPda(binding, nonce) {
|
|
34
|
-
const nonceBuf =
|
|
35
|
-
nonceBuf.writeBigUInt64LE(nonce);
|
|
33
|
+
const nonceBuf = (0, u64_1.u64le)(nonce);
|
|
36
34
|
return web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("claim"), binding.toBuffer(), nonceBuf], ids_1.HARBOR_PROGRAM_ID);
|
|
37
35
|
}
|
|
38
36
|
function channelPda(channelProgram, payer, payee, mint, authorizedSigner, salt, openSlot) {
|
|
39
|
-
const saltBuf =
|
|
40
|
-
|
|
41
|
-
const slotBuf = Buffer.alloc(8);
|
|
42
|
-
slotBuf.writeBigUInt64LE(openSlot);
|
|
37
|
+
const saltBuf = (0, u64_1.u64le)(salt);
|
|
38
|
+
const slotBuf = (0, u64_1.u64le)(openSlot);
|
|
43
39
|
return web3_js_1.PublicKey.findProgramAddressSync([
|
|
44
40
|
Buffer.from("channel"),
|
|
45
41
|
payer.toBuffer(),
|
package/dist/src/receipt.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.RECEIPT_MESSAGE_LEN = void 0;
|
|
4
4
|
exports.receiptMessageBytes = receiptMessageBytes;
|
|
5
|
+
const u64_1 = require("./u64");
|
|
5
6
|
function assertLen(b, n, name) {
|
|
6
7
|
if (b.length !== n)
|
|
7
8
|
throw new Error(`${name} must be ${n} bytes`);
|
|
@@ -16,7 +17,7 @@ function receiptMessageBytes(f) {
|
|
|
16
17
|
o += 32;
|
|
17
18
|
f.binding.toBuffer().copy(out, o);
|
|
18
19
|
o += 32;
|
|
19
|
-
|
|
20
|
+
(0, u64_1.writeU64LE)(out, f.cumulativeSpend, o);
|
|
20
21
|
o += 8;
|
|
21
22
|
Buffer.from(f.meterHash).copy(out, o);
|
|
22
23
|
o += 32;
|
|
@@ -24,9 +25,9 @@ function receiptMessageBytes(f) {
|
|
|
24
25
|
o += 32;
|
|
25
26
|
out.writeUInt8(f.status, o);
|
|
26
27
|
o += 1;
|
|
27
|
-
|
|
28
|
+
(0, u64_1.writeU64LE)(out, f.nonce, o);
|
|
28
29
|
o += 8;
|
|
29
|
-
|
|
30
|
+
(0, u64_1.writeU64LE)(out, f.expirySlot, o);
|
|
30
31
|
o += 8;
|
|
31
32
|
f.signer.toBuffer().copy(out, o);
|
|
32
33
|
o += 32;
|
package/dist/src/tx.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Connection, Signer, Transaction, TransactionSignature } from "@solana/web3.js";
|
|
2
|
+
/** True only for errors proving the tx can never confirm (safe to rebuild). */
|
|
3
|
+
export declare function isExpiredError(e: unknown): boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Send with fresh-blockhash retries. Each attempt rebuilds the tx so a
|
|
6
|
+
* new blockhash is fetched; only expiry-proven failures retry (timeouts
|
|
7
|
+
* and unknown states propagate — resending those could double-execute).
|
|
8
|
+
* All Harbor ixs are init-or-check guarded, so a rebuilt resend after a
|
|
9
|
+
* proven expiry fails safe even in the worst case.
|
|
10
|
+
*/
|
|
11
|
+
export declare function sendWithRetry(conn: Connection, buildTx: () => Transaction, signers: Signer[], maxTries?: number): Promise<TransactionSignature>;
|
package/dist/src/tx.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isExpiredError = isExpiredError;
|
|
4
|
+
exports.sendWithRetry = sendWithRetry;
|
|
5
|
+
const web3_js_1 = require("@solana/web3.js");
|
|
6
|
+
const EXPIRY = /block height exceeded|blockhash not found/i;
|
|
7
|
+
/** True only for errors proving the tx can never confirm (safe to rebuild). */
|
|
8
|
+
function isExpiredError(e) {
|
|
9
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
10
|
+
return EXPIRY.test(msg);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Send with fresh-blockhash retries. Each attempt rebuilds the tx so a
|
|
14
|
+
* new blockhash is fetched; only expiry-proven failures retry (timeouts
|
|
15
|
+
* and unknown states propagate — resending those could double-execute).
|
|
16
|
+
* All Harbor ixs are init-or-check guarded, so a rebuilt resend after a
|
|
17
|
+
* proven expiry fails safe even in the worst case.
|
|
18
|
+
*/
|
|
19
|
+
async function sendWithRetry(conn, buildTx, signers, maxTries = 3) {
|
|
20
|
+
let lastErr = new Error("sendWithRetry: no attempts made");
|
|
21
|
+
for (let attempt = 1; attempt <= maxTries; attempt++) {
|
|
22
|
+
try {
|
|
23
|
+
return await (0, web3_js_1.sendAndConfirmTransaction)(conn, buildTx(), signers);
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
lastErr = e;
|
|
27
|
+
if (attempt >= maxTries || !isExpiredError(e))
|
|
28
|
+
throw e;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
throw lastErr;
|
|
32
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portable little-endian 64-bit encoders.
|
|
3
|
+
*
|
|
4
|
+
* Browser Buffer polyfills predate the BigInt read/write methods, so the
|
|
5
|
+
* SDK must never call writeBigUInt64LE / writeBigInt64LE directly — any
|
|
6
|
+
* bundler-shipped dapp (including our own web UI) would crash at runtime.
|
|
7
|
+
* Index assignment works on every Buffer/Uint8Array implementation.
|
|
8
|
+
*/
|
|
9
|
+
export declare function writeU64LE(out: Uint8Array, v: bigint | number, offset: number): void;
|
|
10
|
+
export declare function writeI64LE(out: Uint8Array, v: bigint | number, offset: number): void;
|
|
11
|
+
export declare function u64le(v: bigint | number): Buffer;
|
package/dist/src/u64.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.writeU64LE = writeU64LE;
|
|
4
|
+
exports.writeI64LE = writeI64LE;
|
|
5
|
+
exports.u64le = u64le;
|
|
6
|
+
/**
|
|
7
|
+
* Portable little-endian 64-bit encoders.
|
|
8
|
+
*
|
|
9
|
+
* Browser Buffer polyfills predate the BigInt read/write methods, so the
|
|
10
|
+
* SDK must never call writeBigUInt64LE / writeBigInt64LE directly — any
|
|
11
|
+
* bundler-shipped dapp (including our own web UI) would crash at runtime.
|
|
12
|
+
* Index assignment works on every Buffer/Uint8Array implementation.
|
|
13
|
+
*/
|
|
14
|
+
function writeU64LE(out, v, offset) {
|
|
15
|
+
let x = typeof v === "bigint" ? v : BigInt(v);
|
|
16
|
+
if (x < 0n || x >= 1n << 64n)
|
|
17
|
+
throw new RangeError("u64 out of range");
|
|
18
|
+
for (let i = 0; i < 8; i++) {
|
|
19
|
+
out[offset + i] = Number(x & 0xffn);
|
|
20
|
+
x >>= 8n;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function writeI64LE(out, v, offset) {
|
|
24
|
+
let x = typeof v === "bigint" ? v : BigInt(v);
|
|
25
|
+
if (x < -(1n << 63n) || x >= 1n << 63n)
|
|
26
|
+
throw new RangeError("i64 out of range");
|
|
27
|
+
if (x < 0n)
|
|
28
|
+
x += 1n << 64n;
|
|
29
|
+
writeU64LE(out, x, offset);
|
|
30
|
+
}
|
|
31
|
+
function u64le(v) {
|
|
32
|
+
const b = Buffer.alloc(8);
|
|
33
|
+
writeU64LE(b, v, 0);
|
|
34
|
+
return b;
|
|
35
|
+
}
|
|
@@ -141,3 +141,74 @@ const P = (n) => new web3_js_1.PublicKey(Buffer.alloc(32, n));
|
|
|
141
141
|
strict_1.default.equal(d.claimSpend, 3670n);
|
|
142
142
|
});
|
|
143
143
|
});
|
|
144
|
+
(0, node_test_1.describe)("sendWithRetry", () => {
|
|
145
|
+
function mockConn(script) {
|
|
146
|
+
let builds = 0;
|
|
147
|
+
const conn = {
|
|
148
|
+
getLatestBlockhash: async () => ({
|
|
149
|
+
blockhash: "11111111111111111111111111111111",
|
|
150
|
+
lastValidBlockHeight: 1,
|
|
151
|
+
}),
|
|
152
|
+
sendTransaction: async () => {
|
|
153
|
+
const step = script.shift();
|
|
154
|
+
if (step === "expiry") {
|
|
155
|
+
throw new Error("Signature X has expired: block height exceeded.");
|
|
156
|
+
}
|
|
157
|
+
if (step === "fatal")
|
|
158
|
+
throw new Error("simulated failure");
|
|
159
|
+
return "sig-ok";
|
|
160
|
+
},
|
|
161
|
+
confirmTransaction: async () => ({ value: { err: null } }),
|
|
162
|
+
};
|
|
163
|
+
return {
|
|
164
|
+
conn,
|
|
165
|
+
builds: () => builds,
|
|
166
|
+
build: () => {
|
|
167
|
+
builds += 1;
|
|
168
|
+
return new web3_js_1.Transaction();
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
(0, node_test_1.it)("rebuilds and resends once after a single expiry", async () => {
|
|
173
|
+
const { conn, builds, build } = mockConn(["expiry", "ok"]);
|
|
174
|
+
const sig = await (0, index_1.sendWithRetry)(conn, build, [], 3);
|
|
175
|
+
strict_1.default.equal(sig, "sig-ok");
|
|
176
|
+
strict_1.default.equal(builds(), 2);
|
|
177
|
+
});
|
|
178
|
+
(0, node_test_1.it)("gives up after maxTries on repeated expiry", async () => {
|
|
179
|
+
const { conn, builds, build } = mockConn(["expiry", "expiry", "expiry"]);
|
|
180
|
+
await strict_1.default.rejects((0, index_1.sendWithRetry)(conn, build, [], 3), /block height exceeded/);
|
|
181
|
+
strict_1.default.equal(builds(), 3);
|
|
182
|
+
});
|
|
183
|
+
(0, node_test_1.it)("propagates non-expiry failures immediately without rebuild", async () => {
|
|
184
|
+
const { conn, builds, build } = mockConn(["fatal", "ok"]);
|
|
185
|
+
await strict_1.default.rejects((0, index_1.sendWithRetry)(conn, build, [], 3), /simulated failure/);
|
|
186
|
+
strict_1.default.equal(builds(), 1);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
(0, node_test_1.describe)("portable u64 encoders", () => {
|
|
190
|
+
(0, node_test_1.it)("matches native writeBigUInt64LE byte-for-byte", () => {
|
|
191
|
+
for (const v of [0n, 1n, 255n, 256n, 2n ** 32n, 2n ** 64n - 1n]) {
|
|
192
|
+
const expected = Buffer.alloc(8);
|
|
193
|
+
expected.writeBigUInt64LE(v);
|
|
194
|
+
strict_1.default.deepEqual((0, index_1.u64le)(v), expected);
|
|
195
|
+
const out = Buffer.alloc(8);
|
|
196
|
+
(0, index_1.writeU64LE)(out, v, 0);
|
|
197
|
+
strict_1.default.deepEqual(out, expected);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
(0, node_test_1.it)("encodes signed i64 incl. negatives like writeBigInt64LE", () => {
|
|
201
|
+
for (const v of [0n, 1n, -1n, -(2n ** 63n), 2n ** 63n - 1n]) {
|
|
202
|
+
const expected = Buffer.alloc(8);
|
|
203
|
+
expected.writeBigInt64LE(v);
|
|
204
|
+
const out = Buffer.alloc(8);
|
|
205
|
+
(0, index_1.writeI64LE)(out, v, 0);
|
|
206
|
+
strict_1.default.deepEqual(out, expected);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
(0, node_test_1.it)("rejects out-of-range values instead of wrapping", () => {
|
|
210
|
+
strict_1.default.throws(() => (0, index_1.u64le)(-1n), RangeError);
|
|
211
|
+
strict_1.default.throws(() => (0, index_1.u64le)(2n ** 64n), RangeError);
|
|
212
|
+
strict_1.default.throws(() => (0, index_1.writeI64LE)(Buffer.alloc(8), 2n ** 63n, 0), RangeError);
|
|
213
|
+
});
|
|
214
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@infantmen-labs/harbor-sdk",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
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": [
|