metered-protocol 1.0.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.
Files changed (206) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +103 -0
  3. package/contracts/metered.ag +135 -0
  4. package/contracts/metered_ag.tests.json +1226 -0
  5. package/contracts/metered_session.sil +237 -0
  6. package/contracts/metered_session.tests.json +1113 -0
  7. package/dist/contracts/metered.ag +135 -0
  8. package/dist/contracts/metered_ag.tests.json +1226 -0
  9. package/dist/contracts/metered_session.sil +237 -0
  10. package/dist/contracts/metered_session.tests.json +1113 -0
  11. package/dist/src/bias.d.ts +48 -0
  12. package/dist/src/bias.d.ts.map +1 -0
  13. package/dist/src/bias.js +43 -0
  14. package/dist/src/bias.js.map +1 -0
  15. package/dist/src/checkpoint.d.ts +57 -0
  16. package/dist/src/checkpoint.d.ts.map +1 -0
  17. package/dist/src/checkpoint.js +84 -0
  18. package/dist/src/checkpoint.js.map +1 -0
  19. package/dist/src/deadline.d.ts +71 -0
  20. package/dist/src/deadline.d.ts.map +1 -0
  21. package/dist/src/deadline.js +77 -0
  22. package/dist/src/deadline.js.map +1 -0
  23. package/dist/src/encoding.d.ts +72 -0
  24. package/dist/src/encoding.d.ts.map +1 -0
  25. package/dist/src/encoding.js +187 -0
  26. package/dist/src/encoding.js.map +1 -0
  27. package/dist/src/history.d.ts +33 -0
  28. package/dist/src/history.d.ts.map +1 -0
  29. package/dist/src/history.js +38 -0
  30. package/dist/src/history.js.map +1 -0
  31. package/dist/src/http/buyer.d.ts +57 -0
  32. package/dist/src/http/buyer.d.ts.map +1 -0
  33. package/dist/src/http/buyer.js +151 -0
  34. package/dist/src/http/buyer.js.map +1 -0
  35. package/dist/src/http/client.d.ts +55 -0
  36. package/dist/src/http/client.d.ts.map +1 -0
  37. package/dist/src/http/client.js +83 -0
  38. package/dist/src/http/client.js.map +1 -0
  39. package/dist/src/http/harness.d.ts +18 -0
  40. package/dist/src/http/harness.d.ts.map +1 -0
  41. package/dist/src/http/harness.js +16 -0
  42. package/dist/src/http/harness.js.map +1 -0
  43. package/dist/src/http/protocol.d.ts +84 -0
  44. package/dist/src/http/protocol.d.ts.map +1 -0
  45. package/dist/src/http/protocol.js +22 -0
  46. package/dist/src/http/protocol.js.map +1 -0
  47. package/dist/src/http/provider.d.ts +123 -0
  48. package/dist/src/http/provider.d.ts.map +1 -0
  49. package/dist/src/http/provider.js +197 -0
  50. package/dist/src/http/provider.js.map +1 -0
  51. package/dist/src/http/serve.d.ts +34 -0
  52. package/dist/src/http/serve.d.ts.map +1 -0
  53. package/dist/src/http/serve.js +171 -0
  54. package/dist/src/http/serve.js.map +1 -0
  55. package/dist/src/http/service.d.ts +86 -0
  56. package/dist/src/http/service.d.ts.map +1 -0
  57. package/dist/src/http/service.js +161 -0
  58. package/dist/src/http/service.js.map +1 -0
  59. package/dist/src/index.d.ts +68 -0
  60. package/dist/src/index.d.ts.map +1 -0
  61. package/dist/src/index.js +62 -0
  62. package/dist/src/index.js.map +1 -0
  63. package/dist/src/meter.d.ts +39 -0
  64. package/dist/src/meter.d.ts.map +1 -0
  65. package/dist/src/meter.js +100 -0
  66. package/dist/src/meter.js.map +1 -0
  67. package/dist/src/offer.d.ts +6 -0
  68. package/dist/src/offer.d.ts.map +1 -0
  69. package/dist/src/offer.js +92 -0
  70. package/dist/src/offer.js.map +1 -0
  71. package/dist/src/reconcile.d.ts +18 -0
  72. package/dist/src/reconcile.d.ts.map +1 -0
  73. package/dist/src/reconcile.js +101 -0
  74. package/dist/src/reconcile.js.map +1 -0
  75. package/dist/src/reservation.d.ts +51 -0
  76. package/dist/src/reservation.d.ts.map +1 -0
  77. package/dist/src/reservation.js +124 -0
  78. package/dist/src/reservation.js.map +1 -0
  79. package/dist/src/session.d.ts +40 -0
  80. package/dist/src/session.d.ts.map +1 -0
  81. package/dist/src/session.js +92 -0
  82. package/dist/src/session.js.map +1 -0
  83. package/dist/src/signer.d.ts +31 -0
  84. package/dist/src/signer.d.ts.map +1 -0
  85. package/dist/src/signer.js +57 -0
  86. package/dist/src/signer.js.map +1 -0
  87. package/dist/src/store.d.ts +30 -0
  88. package/dist/src/store.d.ts.map +1 -0
  89. package/dist/src/store.js +120 -0
  90. package/dist/src/store.js.map +1 -0
  91. package/dist/src/types.d.ts +80 -0
  92. package/dist/src/types.d.ts.map +1 -0
  93. package/dist/src/types.js +9 -0
  94. package/dist/src/types.js.map +1 -0
  95. package/dist/tools/anchor-cli.d.ts +2 -0
  96. package/dist/tools/anchor-cli.d.ts.map +1 -0
  97. package/dist/tools/anchor-cli.js +101 -0
  98. package/dist/tools/anchor-cli.js.map +1 -0
  99. package/dist/tools/anchor.d.ts +68 -0
  100. package/dist/tools/anchor.d.ts.map +1 -0
  101. package/dist/tools/anchor.js +136 -0
  102. package/dist/tools/anchor.js.map +1 -0
  103. package/dist/tools/chain.d.ts +18 -0
  104. package/dist/tools/chain.d.ts.map +1 -0
  105. package/dist/tools/chain.js +18 -0
  106. package/dist/tools/chain.js.map +1 -0
  107. package/dist/tools/conformance.d.ts +2 -0
  108. package/dist/tools/conformance.d.ts.map +1 -0
  109. package/dist/tools/conformance.js +220 -0
  110. package/dist/tools/conformance.js.map +1 -0
  111. package/dist/tools/contracts.d.ts +2 -0
  112. package/dist/tools/contracts.d.ts.map +1 -0
  113. package/dist/tools/contracts.js +68 -0
  114. package/dist/tools/contracts.js.map +1 -0
  115. package/dist/tools/covenant-binding.d.ts +40 -0
  116. package/dist/tools/covenant-binding.d.ts.map +1 -0
  117. package/dist/tools/covenant-binding.js +60 -0
  118. package/dist/tools/covenant-binding.js.map +1 -0
  119. package/dist/tools/covenant-profile.d.ts +41 -0
  120. package/dist/tools/covenant-profile.d.ts.map +1 -0
  121. package/dist/tools/covenant-profile.js +74 -0
  122. package/dist/tools/covenant-profile.js.map +1 -0
  123. package/dist/tools/covenant.d.ts +30 -0
  124. package/dist/tools/covenant.d.ts.map +1 -0
  125. package/dist/tools/covenant.js +54 -0
  126. package/dist/tools/covenant.js.map +1 -0
  127. package/dist/tools/demo-report.d.ts +24 -0
  128. package/dist/tools/demo-report.d.ts.map +1 -0
  129. package/dist/tools/demo-report.js +40 -0
  130. package/dist/tools/demo-report.js.map +1 -0
  131. package/dist/tools/demo.d.ts +2 -0
  132. package/dist/tools/demo.d.ts.map +1 -0
  133. package/dist/tools/demo.js +204 -0
  134. package/dist/tools/demo.js.map +1 -0
  135. package/dist/tools/dust-map.d.ts +2 -0
  136. package/dist/tools/dust-map.d.ts.map +1 -0
  137. package/dist/tools/dust-map.js +63 -0
  138. package/dist/tools/dust-map.js.map +1 -0
  139. package/dist/tools/expire-cases.d.ts +4 -0
  140. package/dist/tools/expire-cases.d.ts.map +1 -0
  141. package/dist/tools/expire-cases.js +211 -0
  142. package/dist/tools/expire-cases.js.map +1 -0
  143. package/dist/tools/fee-check.d.ts +2 -0
  144. package/dist/tools/fee-check.d.ts.map +1 -0
  145. package/dist/tools/fee-check.js +127 -0
  146. package/dist/tools/fee-check.js.map +1 -0
  147. package/dist/tools/fixtures.d.ts +39 -0
  148. package/dist/tools/fixtures.d.ts.map +1 -0
  149. package/dist/tools/fixtures.js +31 -0
  150. package/dist/tools/fixtures.js.map +1 -0
  151. package/dist/tools/gen-contract-tests.d.ts +2 -0
  152. package/dist/tools/gen-contract-tests.d.ts.map +1 -0
  153. package/dist/tools/gen-contract-tests.js +170 -0
  154. package/dist/tools/gen-contract-tests.js.map +1 -0
  155. package/dist/tools/kaspa.d.ts +13 -0
  156. package/dist/tools/kaspa.d.ts.map +1 -0
  157. package/dist/tools/kaspa.js +76 -0
  158. package/dist/tools/kaspa.js.map +1 -0
  159. package/dist/tools/live-deadline.d.ts +2 -0
  160. package/dist/tools/live-deadline.d.ts.map +1 -0
  161. package/dist/tools/live-deadline.js +122 -0
  162. package/dist/tools/live-deadline.js.map +1 -0
  163. package/dist/tools/live-expire.d.ts +2 -0
  164. package/dist/tools/live-expire.d.ts.map +1 -0
  165. package/dist/tools/live-expire.js +127 -0
  166. package/dist/tools/live-expire.js.map +1 -0
  167. package/dist/tools/live-settle.d.ts +2 -0
  168. package/dist/tools/live-settle.d.ts.map +1 -0
  169. package/dist/tools/live-settle.js +226 -0
  170. package/dist/tools/live-settle.js.map +1 -0
  171. package/dist/tools/live-steps.d.ts +63 -0
  172. package/dist/tools/live-steps.d.ts.map +1 -0
  173. package/dist/tools/live-steps.js +155 -0
  174. package/dist/tools/live-steps.js.map +1 -0
  175. package/dist/tools/model.d.ts +32 -0
  176. package/dist/tools/model.d.ts.map +1 -0
  177. package/dist/tools/model.js +99 -0
  178. package/dist/tools/model.js.map +1 -0
  179. package/dist/tools/send.d.ts +2 -0
  180. package/dist/tools/send.d.ts.map +1 -0
  181. package/dist/tools/send.js +41 -0
  182. package/dist/tools/send.js.map +1 -0
  183. package/dist/tools/session-chain.d.ts +69 -0
  184. package/dist/tools/session-chain.d.ts.map +1 -0
  185. package/dist/tools/session-chain.js +113 -0
  186. package/dist/tools/session-chain.js.map +1 -0
  187. package/dist/tools/sighash.d.ts +9 -0
  188. package/dist/tools/sighash.d.ts.map +1 -0
  189. package/dist/tools/sighash.js +68 -0
  190. package/dist/tools/sighash.js.map +1 -0
  191. package/dist/tools/sigscript-check.d.ts +2 -0
  192. package/dist/tools/sigscript-check.d.ts.map +1 -0
  193. package/dist/tools/sigscript-check.js +113 -0
  194. package/dist/tools/sigscript-check.js.map +1 -0
  195. package/dist/tools/sigscript.d.ts +61 -0
  196. package/dist/tools/sigscript.d.ts.map +1 -0
  197. package/dist/tools/sigscript.js +70 -0
  198. package/dist/tools/sigscript.js.map +1 -0
  199. package/dist/tools/state-layout.d.ts +2 -0
  200. package/dist/tools/state-layout.d.ts.map +1 -0
  201. package/dist/tools/state-layout.js +57 -0
  202. package/dist/tools/state-layout.js.map +1 -0
  203. package/package.json +59 -0
  204. package/spec/CONFORMANCE.md +80 -0
  205. package/spec/SPEC.md +821 -0
  206. package/spec/conformance-vectors.json +677 -0
@@ -0,0 +1,13 @@
1
+ export type Network = 'testnet-10' | 'testnet-11' | 'mainnet';
2
+ export declare const KEY_FILE: string;
3
+ type Sdk = any;
4
+ /** Loads the vendored rusty-kaspa WASM bindings. Node 21+ supplies the WebSocket global itself. */
5
+ export declare function loadSdk(): Promise<Sdk>;
6
+ /** The anchor key, or null if none has been generated. Never printed by anything here. */
7
+ export declare function loadAnchorKey(): string | null;
8
+ /** Writes a key to ~/.metered/kaspa.env. Refuses to overwrite: losing one loses its funds. */
9
+ export declare function saveAnchorKey(privateKeyHex: string): void;
10
+ /** The address this key pays from, which is also where a faucet should send test coins. */
11
+ export declare function anchorAddress(privateKeyHex: string, network: Network): Promise<string>;
12
+ export {};
13
+ //# sourceMappingURL=kaspa.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kaspa.d.ts","sourceRoot":"","sources":["../../tools/kaspa.ts"],"names":[],"mappings":"AAmBA,MAAM,MAAM,OAAO,GAAG,YAAY,GAAG,YAAY,GAAG,SAAS,CAAC;AAa9D,eAAO,MAAM,QAAQ,QAA2C,CAAC;AAGjE,KAAK,GAAG,GAAG,GAAG,CAAC;AAWf,mGAAmG;AACnG,wBAAsB,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,CAe5C;AAED,0FAA0F;AAC1F,wBAAgB,aAAa,IAAI,MAAM,GAAG,IAAI,CAI7C;AAED,8FAA8F;AAC9F,wBAAgB,aAAa,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAUzD;AAED,2FAA2F;AAC3F,wBAAsB,aAAa,CAAC,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAI5F"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Loading the Kaspa SDK, and holding the key that pays for anchors.
3
+ *
4
+ * THE SDK IS NOT VENDORED HERE, deliberately. It is a 12 MB WASM blob, and this repository's whole
5
+ * posture is that everything it contains can be verified by CI on a clean checkout. The same
6
+ * arrangement already applies to `silverc` and the debugger: a heavy external artefact is found by
7
+ * environment variable and its absence fails with instructions rather than a stack trace. The
8
+ * encoder was vendored because the golden vector could not otherwise be regenerated; nothing about
9
+ * a checkpoint anchor has that property, because anchoring needs a live node either way.
10
+ *
11
+ * THE KEY LIVES OUTSIDE THE REPOSITORY, at ~/.metered/kaspa.env, for the reason the provider keys
12
+ * do: .gitignore stops a commit and does not stop a read, and anything working on this project can
13
+ * read anything inside it.
14
+ */
15
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
16
+ import { join } from 'node:path';
17
+ import { homedir } from 'node:os';
18
+ import { pathToFileURL } from 'node:url';
19
+ /**
20
+ * The rusty-kaspa WASM bindings, which are NOT vendored here: 12 MB of WASM with its own release
21
+ * cadence. Point METERED_KASPA_SDK at `kaspa.js` from a nodejs build, or put the same line in
22
+ * ~/.metered/kaspa.env beside the key -- outside the repository, like everything else that is
23
+ * specific to one machine.
24
+ *
25
+ * Only the chain tooling needs it. The specification, the conformance vectors and both
26
+ * implementations run without it.
27
+ */
28
+ const SDK_ENV = 'METERED_KASPA_SDK';
29
+ export const KEY_FILE = join(homedir(), '.metered', 'kaspa.env');
30
+ let cached = null;
31
+ /** Read a NAME=value line from the key file, so a machine-specific path stays out of the repository. */
32
+ function fromKeyFile(name) {
33
+ if (!existsSync(KEY_FILE))
34
+ return undefined;
35
+ const match = readFileSync(KEY_FILE, 'utf8').match(new RegExp(`^${name}=(.+?)[ \\t\\r]*$`, 'm'));
36
+ return match?.[1];
37
+ }
38
+ /** Loads the vendored rusty-kaspa WASM bindings. Node 21+ supplies the WebSocket global itself. */
39
+ export async function loadSdk() {
40
+ if (cached)
41
+ return cached;
42
+ const entry = process.env[SDK_ENV] ?? fromKeyFile(SDK_ENV);
43
+ if (!entry || !existsSync(entry)) {
44
+ throw new Error(`Kaspa SDK not found${entry ? ` at ${entry}` : ''}.\n\n` +
45
+ `Set ${SDK_ENV} to kaspa.js from a rusty-kaspa WASM nodejs build, or add\n` +
46
+ `${SDK_ENV}=<path> to ${KEY_FILE}.`);
47
+ }
48
+ const imported = await import(pathToFileURL(entry).href);
49
+ const sdk = (imported.default ?? imported);
50
+ sdk.initConsolePanicHook?.();
51
+ cached = sdk;
52
+ return sdk;
53
+ }
54
+ /** The anchor key, or null if none has been generated. Never printed by anything here. */
55
+ export function loadAnchorKey() {
56
+ if (!existsSync(KEY_FILE))
57
+ return null;
58
+ const match = readFileSync(KEY_FILE, 'utf8').match(/^METERED_ANCHOR_KEY=([0-9a-fA-F]{64})\s*$/m);
59
+ return match ? match[1].toLowerCase() : null;
60
+ }
61
+ /** Writes a key to ~/.metered/kaspa.env. Refuses to overwrite: losing one loses its funds. */
62
+ export function saveAnchorKey(privateKeyHex) {
63
+ if (loadAnchorKey())
64
+ throw new Error(`${KEY_FILE} already holds a key; refusing to overwrite it`);
65
+ mkdirSync(join(homedir(), '.metered'), { recursive: true });
66
+ writeFileSync(KEY_FILE, '# Metered -- the key that pays for checkpoint anchors. TESTNET ONLY.\n' +
67
+ '# Outside the repository on purpose: .gitignore stops a commit, not a read.\n' +
68
+ `METERED_ANCHOR_KEY=${privateKeyHex}\n`, { mode: 0o600 });
69
+ }
70
+ /** The address this key pays from, which is also where a faucet should send test coins. */
71
+ export async function anchorAddress(privateKeyHex, network) {
72
+ const sdk = await loadSdk();
73
+ const priv = new sdk.PrivateKey(privateKeyHex);
74
+ return priv.toKeypair().toAddress(new sdk.NetworkId(network)).toString();
75
+ }
76
+ //# sourceMappingURL=kaspa.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kaspa.js","sourceRoot":"","sources":["../../tools/kaspa.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=live-deadline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"live-deadline.d.ts","sourceRoot":"","sources":["../../tools/live-deadline.ts"],"names":[],"mappings":""}
@@ -0,0 +1,122 @@
1
+ /**
2
+ * `npm run live:deadline` -- prove SPEC.md 7.3a on chain: what a provider loses by not posting.
3
+ *
4
+ * A real session runs over HTTP. Both parties count, both sign, and the provider ends up holding
5
+ * doubly-signed States for work it genuinely delivered. Then it does the one thing 7.3a forbids:
6
+ * nothing. The covenant UTXO ages past the response window with no claim pending, the buyer calls
7
+ * `expire`, and consensus hands the entire balance back to the buyer.
8
+ *
9
+ * THE POINT IS THAT NOTHING HERE IS AN ATTACK. The buyer does not forge, race, or exploit; it
10
+ * waits and then follows the rules. The provider is not cheated -- it holds perfectly good
11
+ * evidence of a debt, and the chain releases the money anyway, because a signed State that was
12
+ * never posted is a claim nobody made.
13
+ *
14
+ * This runs on testnet-10 and costs test coins. It is the demonstration behind the rule.
15
+ */
16
+ import { mkdtempSync } from 'node:fs';
17
+ import { tmpdir } from 'node:os';
18
+ import { join } from 'node:path';
19
+ import { randomBytes } from 'node:crypto';
20
+ import { bytesToHex } from '@noble/hashes/utils';
21
+ import { publicKeyHex } from '../src/encoding.js';
22
+ import { serveMetered } from '../src/http/serve.js';
23
+ import { MeteredService } from '../src/http/service.js';
24
+ import { openSession, runBabel } from '../src/http/client.js';
25
+ import { meterFor } from '../src/meter.js';
26
+ import { shouldSettle } from '../src/deadline.js';
27
+ import { loadSdk, loadAnchorKey } from './kaspa.js';
28
+ import { compileWithState, covenantAddress } from './covenant.js';
29
+ import { awaitUtxo, buildExpire, awaitWindow } from './live-steps.js';
30
+ const NETWORK = 'testnet-10';
31
+ const FUND = 50000000n;
32
+ const FUND_FEE = 250000n;
33
+ const CLOSE_FEE = 400000n;
34
+ const WINDOW = 2;
35
+ const PRICE = 3630;
36
+ const BABEL = 10;
37
+ const dir = mkdtempSync(join(tmpdir(), 'metered-deadline-'));
38
+ const meter = meterFor('o200k_base');
39
+ const deliver = (prompt, max) => new TextEncoder().encode(Array.from({ length: max }, (_, i) => `${prompt}${i}`).join(' '));
40
+ /** What the provider's policy WOULD have told it, had it been asked. */
41
+ const POLICY = { settleByAgeDaa: 1, settleAtUnsettledSompi: 50_000 };
42
+ async function main() {
43
+ const funder = loadAnchorKey();
44
+ if (!funder)
45
+ throw new Error('no anchor key; run: npm run anchor -- address');
46
+ const buyerSk = bytesToHex(randomBytes(32));
47
+ const providerSk = bytesToHex(randomBytes(32));
48
+ const terms = {
49
+ v: 1, scheme: 'metered', network: 'kaspa:testnet-10', asset: 'KAS',
50
+ unit: 'llm.output_tokens.v1', meter: 'o200k_base',
51
+ unitPriceSompi: PRICE, babelUnits: BABEL, maxBabels: 16,
52
+ toleranceAbs: 1, checkpointEvery: 0, responseWindowDaa: WINDOW,
53
+ };
54
+ const service = new MeteredService({
55
+ terms, providerSk, providerPubkey: publicKeyHex(providerSk), meter, deliver,
56
+ });
57
+ const server = serveMetered({ service });
58
+ await new Promise((r) => server.listen(0, '127.0.0.1', r));
59
+ const base = `http://127.0.0.1:${server.address().port}`;
60
+ const sdk = await loadSdk();
61
+ const networkId = new sdk.NetworkId(NETWORK);
62
+ const priv = new sdk.PrivateKey(funder);
63
+ const from = priv.toKeypair().toAddress(networkId).toString();
64
+ const buyerAddr = new sdk.PrivateKey(buyerSk).toKeypair().toAddress(networkId).toString();
65
+ const rpc = new sdk.RpcClient({ resolver: new sdk.Resolver(), encoding: sdk.Encoding.Borsh, networkId });
66
+ await rpc.connect();
67
+ try {
68
+ const { offer, session } = await openSession(base, buyerSk, meter, 'kaspa:testnet-10');
69
+ const built = compileWithState(dir, {
70
+ parties: offer.partiesCommitment, sessionId: offer.sessionId, window: WINDOW, seq: -1, sompi: 0,
71
+ });
72
+ const address = await covenantAddress(built.hex, NETWORK);
73
+ const { entries } = await rpc.getUtxosByAddresses([from]);
74
+ const src = entries.reduce((a, b) => (b.amount > a.amount ? b : a));
75
+ const fund = sdk.createTransaction([src], [{ address, amount: FUND }, { address: from, amount: src.amount - FUND - FUND_FEE }], 0n, undefined, 0);
76
+ fund.version = 1;
77
+ fund.gas = 0n;
78
+ for (const i of fund.inputs) {
79
+ i.sigOpCount = 0;
80
+ i.computeBudget = 10;
81
+ }
82
+ fund.finalize();
83
+ await rpc.submitTransaction({ transaction: sdk.signTransaction(fund, [priv], true), allowOrphan: false });
84
+ const utxo = await awaitUtxo(rpc, address, FUND);
85
+ if (!utxo)
86
+ throw new Error('the covenant UTXO never appeared');
87
+ console.log(`\n 1. FUNDED ${Number(FUND) / 1e8} TKAS at ${address.slice(0, 30)}...`);
88
+ // ---- real work, really agreed.
89
+ let owed = 0;
90
+ for (let i = 0; i < 2; i += 1) {
91
+ const out = await runBabel(base, session, `question-${i}-`);
92
+ owed = out.state.cumulativeSompi;
93
+ console.log(` babel ${i}: ${out.billedUnits} units, ${owed} sompi owed and DOUBLY SIGNED`);
94
+ }
95
+ const verdict = shouldSettle(POLICY, { ageDaa: WINDOW, unsettledSompi: owed });
96
+ console.log(`\n 2. POLICY says settle: ${verdict.settle} -- ${verdict.why ?? 'nothing owed'}`);
97
+ console.log(' the provider ignores it and posts nothing. That is the whole experiment.');
98
+ await awaitWindow(rpc, WINDOW);
99
+ console.log(`\n 3. AGED past the ${WINDOW}-DAA window with NO claim pending`);
100
+ const close = buildExpire(sdk, { buyerPk: offer.buyerPubkey, providerPk: offer.providerPubkey, signerSk: buyerSk,
101
+ redeem: built.hex, tag: built.entries.expire?.dispatch_tag ?? '' }, utxo, [{ address: buyerAddr, amount: utxo.amount - CLOSE_FEE }], WINDOW);
102
+ const id = (await rpc.submitTransaction({ transaction: close, allowOrphan: false })).transactionId;
103
+ const got = await awaitUtxo(rpc, buyerAddr, utxo.amount - CLOSE_FEE);
104
+ if (!got)
105
+ throw new Error('the close did not land');
106
+ console.log(`\n 4. CLOSED the buyer took everything back ${id}`);
107
+ console.log(`\n PROVIDER OWED ${owed} sompi, doubly signed, for work actually delivered`);
108
+ console.log(` PROVIDER RECEIVED 0`);
109
+ console.log(` BUYER RECEIVED ${Number(utxo.amount - CLOSE_FEE) / 1e8} TKAS -- the entire balance`);
110
+ console.log('\n Nothing here was an attack. The buyer waited, then followed the rules.');
111
+ console.log(' SPEC.md 7.3a is the rule that makes this the provider\'s own fault.\n');
112
+ }
113
+ finally {
114
+ await new Promise((r) => server.close(() => r()));
115
+ await rpc.disconnect().catch(() => undefined);
116
+ }
117
+ }
118
+ main().catch((err) => {
119
+ console.error(`\n ${err instanceof Error ? err.message : String(err)}\n`);
120
+ process.exit(1);
121
+ });
122
+ //# sourceMappingURL=live-deadline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"live-deadline.js","sourceRoot":"","sources":["../../tools/live-deadline.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=live-expire.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"live-expire.d.ts","sourceRoot":"","sources":["../../tools/live-expire.ts"],"names":[],"mappings":""}
@@ -0,0 +1,127 @@
1
+ /**
2
+ * `npm run live:expire` -- open a real covenant on testnet-10 and close it with `expire`.
3
+ *
4
+ * WHY EXPIRE AND NOT SETTLE. `settle` hands the funds back to the SAME covenant with updated
5
+ * state, and a continuation output must carry a KIP-20 CovenantBinding tying it to the authorizing
6
+ * input. That plumbing now exists in tools/live-settle.ts. `expire` still needs none of it: it
7
+ * pays plain P2PK addresses and continues nothing, and Argent lowers `emits {}` to
8
+ * OpAuthOutputCount == 0, which an unbound payout already satisfies. That is what made it the
9
+ * first covenant path to run on chain, and the first the ARGENT port ran too.
10
+ *
11
+ * WHAT IT PROVES. The whole covenant executes under real consensus: the parties commitment, the
12
+ * relative timelock, the signature check, and the no-claim refund branch that step 3.4 added
13
+ * after finding the funding UTXO would otherwise be unspendable forever.
14
+ *
15
+ * Everything happens in ONE run because the session keys live only in memory. Two earlier
16
+ * attempts generated keys, funded a covenant and then exited on an unrelated error, stranding the
17
+ * coins at an address whose keys no longer exist. Testnet, so it cost nothing but the lesson.
18
+ */
19
+ import { mkdtempSync } from 'node:fs';
20
+ import { tmpdir } from 'node:os';
21
+ import { join } from 'node:path';
22
+ import { randomBytes } from 'node:crypto';
23
+ import { blake3 } from '@noble/hashes/blake3';
24
+ import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
25
+ import { publicKeyHex } from '../src/encoding.js';
26
+ import { loadSdk, loadAnchorKey } from './kaspa.js';
27
+ import { compileWithState, covenantAddress } from './covenant.js';
28
+ import { profileFromArgv } from './covenant-profile.js';
29
+ const NETWORK = 'testnet-10';
30
+ const FUND_SOMPI = 50000000n;
31
+ const FUND_FEE = 250000n;
32
+ // expire does TWO signature checks, so it needs compute budget 21, not 10 -- and mass rises with
33
+ // the budget, so the node demands ~313,800 for a one-output close. Measured by being refused.
34
+ const SPEND_FEE = 350000n;
35
+ const EXPIRE_BUDGET = 21;
36
+ const WINDOW = 2;
37
+ /** kaspa-depin's lesson: createInputSignature returns a signature SCRIPT; strip the 0x41 push. */
38
+ const bareSig = (s) => (s.length === 132 && s.startsWith('41') ? s.slice(2) : s);
39
+ async function main() {
40
+ const funder = loadAnchorKey();
41
+ if (!funder)
42
+ throw new Error('no anchor key; run: npm run anchor -- address');
43
+ const dir = mkdtempSync(join(tmpdir(), 'metered-expire-'));
44
+ const buyerSk = bytesToHex(randomBytes(32));
45
+ const providerSk = bytesToHex(randomBytes(32));
46
+ const buyerPk = publicKeyHex(buyerSk);
47
+ const providerPk = publicKeyHex(providerSk);
48
+ const parties = bytesToHex(blake3(new Uint8Array([...hexToBytes(buyerPk), ...hexToBytes(providerPk)]), { dkLen: 32 }));
49
+ const sessionId = bytesToHex(randomBytes(16));
50
+ // `npm run live:expire -- --profile ag` closes with the ARGENT covenant instead. `expire` is
51
+ // the entry that needs no KIP-20 plumbing: Argent lowers `emits {}` to OpAuthOutputCount == 0,
52
+ // and an unbound P2PK payout already has zero authorised outputs.
53
+ const profile = profileFromArgv();
54
+ const compiled = compileWithState(dir, { parties, sessionId, window: WINDOW, seq: -1, sompi: 0 }, profile.contract);
55
+ const address = await covenantAddress(compiled.hex, NETWORK);
56
+ const tag = compiled.entries.expire?.dispatch_tag;
57
+ if (!tag)
58
+ throw new Error('the artifact has no expire entry');
59
+ console.log(`\n covenant ${compiled.hex.length / 2} bytes [${profile.key}] ${profile.contract}`);
60
+ console.log(` address ${address}`);
61
+ console.log(` closing with EXPIRE, no claim posted -- the buyer takes it all back (SPEC 7.2)`);
62
+ const sdk = await loadSdk();
63
+ const networkId = new sdk.NetworkId(NETWORK);
64
+ const priv = new sdk.PrivateKey(funder);
65
+ const funderAddr = priv.toKeypair().toAddress(networkId).toString();
66
+ const buyerAddr = new sdk.PrivateKey(buyerSk).toKeypair().toAddress(networkId).toString();
67
+ const rpc = new sdk.RpcClient({ resolver: new sdk.Resolver(), encoding: sdk.Encoding.Borsh, networkId });
68
+ await rpc.connect();
69
+ try {
70
+ const { entries } = await rpc.getUtxosByAddresses([funderAddr]);
71
+ const utxo = entries.reduce((a, b) => (b.amount > a.amount ? b : a));
72
+ const fundTx = sdk.createTransaction([utxo], [{ address, amount: FUND_SOMPI }, { address: funderAddr, amount: utxo.amount - FUND_SOMPI - FUND_FEE }], 0n, undefined, 0);
73
+ fundTx.version = 1;
74
+ fundTx.gas = 0n;
75
+ for (const i of fundTx.inputs) {
76
+ i.sigOpCount = 0;
77
+ i.computeBudget = 10;
78
+ }
79
+ fundTx.finalize();
80
+ const fundId = (await rpc.submitTransaction({ transaction: sdk.signTransaction(fundTx, [priv], true), allowOrphan: false })).transactionId;
81
+ console.log(` funded ${Number(FUND_SOMPI) / 1e8} TKAS txid ${fundId}`);
82
+ let cov = null;
83
+ for (let i = 0; i < 40 && !cov; i += 1) {
84
+ await new Promise((r) => setTimeout(r, 1500));
85
+ const res = await rpc.getUtxosByAddresses([address]);
86
+ cov = res.entries.find((e) => e.amount === FUND_SOMPI) ?? null;
87
+ }
88
+ if (!cov)
89
+ throw new Error('the covenant UTXO never appeared');
90
+ // The window is RELATIVE (SPEC 7.3): ageDaa lowers to OpCheckSequenceVerify, which reads the
91
+ // spending input's sequence. The UTXO must also actually BE that old, so wait for the DAA
92
+ // score to advance past it.
93
+ const startDaa = (await rpc.getBlockDagInfo()).virtualDaaScore;
94
+ while ((await rpc.getBlockDagInfo()).virtualDaaScore < startDaa + BigInt(WINDOW) + 2n) {
95
+ await new Promise((r) => setTimeout(r, 1000));
96
+ }
97
+ console.log(` aged past the ${WINDOW}-DAA response window`);
98
+ const spend = sdk.createTransaction([cov], [{ address: buyerAddr, amount: cov.amount - SPEND_FEE }], 0n, undefined, 0);
99
+ spend.version = 1;
100
+ spend.gas = 0n;
101
+ spend.inputs[0].sigOpCount = 0;
102
+ spend.inputs[0].computeBudget = EXPIRE_BUDGET;
103
+ spend.inputs[0].sequence = BigInt(WINDOW);
104
+ const sig = bareSig(sdk.createInputSignature(spend, 0, new sdk.PrivateKey(buyerSk)));
105
+ const sb = new sdk.ScriptBuilder();
106
+ sb.addData(buyerPk);
107
+ sb.addData(providerPk);
108
+ sb.addData(sig);
109
+ sb.addData(tag);
110
+ sb.addData(compiled.hex);
111
+ spend.inputs[0].signatureScript = sb.toString();
112
+ spend.finalize();
113
+ const id = (await rpc.submitTransaction({ transaction: spend, allowOrphan: false })).transactionId;
114
+ console.log(`\n EXPIRED the covenant ran under real consensus and paid out`);
115
+ console.log(` refund ${Number(cov.amount - SPEND_FEE) / 1e8} TKAS -> ${buyerAddr}`);
116
+ console.log(` txid ${id}`);
117
+ console.log(` explorer https://explorer-tn10.kaspa.org/txs/${id}\n`);
118
+ }
119
+ finally {
120
+ await rpc.disconnect().catch(() => undefined);
121
+ }
122
+ }
123
+ main().catch((err) => {
124
+ console.error(`\n ${err instanceof Error ? err.message : String(err)}\n`);
125
+ process.exit(1);
126
+ });
127
+ //# sourceMappingURL=live-expire.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"live-expire.js","sourceRoot":"","sources":["../../tools/live-expire.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=live-settle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"live-settle.d.ts","sourceRoot":"","sources":["../../tools/live-settle.ts"],"names":[],"mappings":""}
@@ -0,0 +1,226 @@
1
+ /**
2
+ * `npm run live:settle` -- drive a real multi-chunk session on testnet-10, including a SUPERSEDE.
3
+ *
4
+ * The single settle that ran first proved the covenant accepts a doubly-signed State. It did not
5
+ * prove the thing the two-phase design exists for: that a LATER State replaces a pending one and
6
+ * a STALE one cannot. This runs all three steps against real consensus --
7
+ *
8
+ * 1. settle seq 0 the first claim posts
9
+ * 2. settle seq 0 again refused; the guard is `seq > pendingSeq`, strictly
10
+ * 3. settle seq 1 supersedes, and the state advances again
11
+ * 4. expire closes it, and the money SPLITS
12
+ *
13
+ * Step 2 is the one worth having. It is the stale-close threat -- "settle an old, lower-seq
14
+ * State" -- and until now it had only ever been refused by a simulator.
15
+ *
16
+ * Step 4 is the other one worth having. Every earlier expire closed a session with NO claim
17
+ * posted -- one output, nothing to divide. This one pays the provider exactly what it is owed and
18
+ * refunds the rest, which is the only branch where getting an output value wrong costs somebody.
19
+ *
20
+ * The simulator is still the pre-flight for every spend EXPECTED to succeed, and it emits the
21
+ * authoritative signature script. See tools/live-steps.ts for why the redeem script must be
22
+ * appended to it by hand.
23
+ */
24
+ import { mkdtempSync } from 'node:fs';
25
+ import { tmpdir } from 'node:os';
26
+ import { join } from 'node:path';
27
+ import { randomBytes } from 'node:crypto';
28
+ import { blake3 } from '@noble/hashes/blake3';
29
+ import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
30
+ import { publicKeyHex, signState, digestHex } from '../src/encoding.js';
31
+ import { loadSdk, loadAnchorKey } from './kaspa.js';
32
+ import { compileWithState, covenantAddress } from './covenant.js';
33
+ import { preflightSigscript, awaitUtxo, withState, buildExpire, awaitWindow, x } from './live-steps.js';
34
+ import { profileFromArgv, COVENANT_ID } from './covenant-profile.js';
35
+ import { bindContinuation, fundAsCovenant, covenantIdOf, assertInherited } from './covenant-binding.js';
36
+ // `npm run live:settle -- --profile ag` drives the ARGENT covenant instead.
37
+ const PROFILE = profileFromArgv();
38
+ const NETWORK = 'testnet-10';
39
+ const FUND_SOMPI = 50000000n;
40
+ const FUND_FEE = 250000n;
41
+ const SPEND_FEE = 360000n; // measured: mass 3,429 -> 342,900 required
42
+ const SPEND_BUDGET = 21; // two checkMsgSig calls
43
+ const WINDOW = 2;
44
+ const CLOSE_FEE = 400000n; // the covenant allowance in full; expire is the costliest path
45
+ const PRICE = 3630;
46
+ const CHUNK = 550;
47
+ const dir = mkdtempSync(join(tmpdir(), 'metered-settle-'));
48
+ /** Build one settle spend: pre-flight it, then hand back a signed transaction ready to submit. */
49
+ function buildSettle(sdk, args, pending, claim, redeem, utxo, outAddress, expectPass) {
50
+ const state = {
51
+ v: 1, sessionId: args.sessionId, seq: claim.seq,
52
+ cumulativeUnits: claim.cumulativeUnits, cumulativeSompi: claim.cumulativeSompi,
53
+ prevState: claim.prevState,
54
+ };
55
+ const ctor = [x(args.parties), x(args.sessionId), WINDOW, pending.seq, pending.sompi];
56
+ const outValue = utxo.amount - SPEND_FEE;
57
+ const sigscript = preflightSigscript({
58
+ name: `live settle seq ${claim.seq}${expectPass ? '' : ' (stale)'}`,
59
+ function: 'settle',
60
+ constructor_args: ctor,
61
+ args: [
62
+ x(args.buyer.pk), x(args.provider.pk),
63
+ x(signState(state, args.buyer.sk)), x(signState(state, args.provider.sk)),
64
+ claim.seq, claim.cumulativeUnits, claim.cumulativeSompi, x(claim.prevState ?? '00'.repeat(32)),
65
+ ],
66
+ expect: expectPass ? 'pass' : 'fail',
67
+ tx: {
68
+ active_input_index: 0,
69
+ inputs: [{ utxo_value: Number(utxo.amount), constructor_args: ctor, ...(PROFILE.binds ? { covenant_id: COVENANT_ID } : {}) }],
70
+ outputs: [{
71
+ value: Number(outValue), constructor_args: ctor,
72
+ state: PROFILE.outState({
73
+ parties: args.parties, sessionId: args.sessionId, window: WINDOW,
74
+ seq: claim.seq, sompi: claim.cumulativeSompi,
75
+ }),
76
+ ...(PROFILE.binds ? { covenant_id: COVENANT_ID, authorizing_input: 0 } : {}),
77
+ }],
78
+ },
79
+ }, dir, expectPass, PROFILE.contract);
80
+ const tx = sdk.createTransaction([utxo], [{ address: outAddress, amount: outValue }], 0n, undefined, 0);
81
+ tx.version = 1;
82
+ tx.gas = 0n;
83
+ tx.inputs[0].sigOpCount = 0;
84
+ tx.inputs[0].computeBudget = SPEND_BUDGET;
85
+ const push = new sdk.ScriptBuilder();
86
+ push.addData(redeem);
87
+ tx.inputs[0].signatureScript = sigscript + push.toString();
88
+ const binding = bindContinuation(sdk, tx, utxo, PROFILE.binds);
89
+ tx.finalize();
90
+ return { tx, state, outValue, binding };
91
+ }
92
+ /**
93
+ * Fund the covenant from the anchor key and wait for its UTXO.
94
+ *
95
+ * AN ARGENT SESSION MUST BE FUNDED AS A COVENANT, not merely paid into, and this cost a rejected
96
+ * transaction to learn. `OpAuthOutputCount(activeInput)` asks how many outputs are authorised by
97
+ * the input being spent -- and an input that belongs to NO covenant authorises nothing, whatever
98
+ * its outputs claim. So the identity has to exist before the first settle, which means the
99
+ * FUNDING transaction is the genesis.
100
+ *
101
+ * The simulator passed the same spend because its test case declares `covenant_id` on the input
102
+ * directly; on chain there was nothing to declare it. That disagreement between simulator and
103
+ * consensus is the finding, and it is why the pre-flight is a floor rather than a proof.
104
+ *
105
+ * The hand-written covenant needs none of this: it constrains `outputs[0]` by convention and
106
+ * never asks who authorised it.
107
+ */
108
+ async function payInto(sdk, rpc, priv, from, address) {
109
+ const { entries } = await rpc.getUtxosByAddresses([from]);
110
+ const source = entries.reduce((a, b) => (b.amount > a.amount ? b : a));
111
+ const tx = sdk.createTransaction([source], [{ address, amount: FUND_SOMPI }, { address: from, amount: source.amount - FUND_SOMPI - FUND_FEE }], 0n, undefined, 0);
112
+ tx.version = 1;
113
+ tx.gas = 0n;
114
+ for (const i of tx.inputs) {
115
+ i.sigOpCount = 0;
116
+ i.computeBudget = 10;
117
+ }
118
+ fundAsCovenant(tx, PROFILE.binds);
119
+ tx.finalize();
120
+ await rpc.submitTransaction({ transaction: sdk.signTransaction(tx, [priv], true), allowOrphan: false });
121
+ return awaitUtxo(rpc, address, FUND_SOMPI);
122
+ }
123
+ /**
124
+ * Submit a spend that MUST be rejected, and return why. A silent success here would mean the
125
+ * supersede guard does not hold on chain, which is the one outcome that must never pass quietly.
126
+ */
127
+ async function expectRefusal(rpc, tx) {
128
+ try {
129
+ await rpc.submitTransaction({ transaction: tx, allowOrphan: false });
130
+ }
131
+ catch (err) {
132
+ return (err instanceof Error ? err.message : String(err)).split(':').pop()?.trim() ?? 'refused';
133
+ }
134
+ throw new Error('CONSENSUS ACCEPTED A STALE STATE -- the supersede guard does not hold on chain');
135
+ }
136
+ async function main() {
137
+ const funder = loadAnchorKey();
138
+ if (!funder)
139
+ throw new Error('no anchor key; run: npm run anchor -- address');
140
+ const buyer = { sk: bytesToHex(randomBytes(32)), pk: '' };
141
+ const provider = { sk: bytesToHex(randomBytes(32)), pk: '' };
142
+ buyer.pk = publicKeyHex(buyer.sk);
143
+ provider.pk = publicKeyHex(provider.sk);
144
+ const parties = bytesToHex(blake3(new Uint8Array([...hexToBytes(buyer.pk), ...hexToBytes(provider.pk)]), { dkLen: 32 }));
145
+ const sessionId = bytesToHex(randomBytes(16));
146
+ const ids = { parties, sessionId, buyer, provider };
147
+ const opened = compileWithState(dir, { parties, sessionId, window: WINDOW, seq: -1, sompi: 0 }, PROFILE.contract);
148
+ const spanEnd = opened.span.offset + opened.span.len;
149
+ const address = await covenantAddress(opened.hex, NETWORK);
150
+ console.log(`\n covenant ${opened.hex.length / 2} bytes [${PROFILE.key}] state span ${opened.span.len}`);
151
+ console.log(` address ${address}`);
152
+ const sdk = await loadSdk();
153
+ const networkId = new sdk.NetworkId(NETWORK);
154
+ const priv = new sdk.PrivateKey(funder);
155
+ const funderAddr = priv.toKeypair().toAddress(networkId).toString();
156
+ const rpc = new sdk.RpcClient({ resolver: new sdk.Resolver(), encoding: sdk.Encoding.Borsh, networkId });
157
+ await rpc.connect();
158
+ try {
159
+ const utxo0 = await payInto(sdk, rpc, priv, funderAddr, address);
160
+ if (!utxo0)
161
+ throw new Error('the covenant UTXO never appeared');
162
+ console.log(` funded ${Number(FUND_SOMPI) / 1e8} TKAS`);
163
+ // ---- 1. the first claim
164
+ const claim0 = { seq: 0, cumulativeUnits: CHUNK, cumulativeSompi: CHUNK * PRICE, prevState: null };
165
+ const redeem1 = withState(opened.hex, claim0.seq, claim0.cumulativeSompi, spanEnd);
166
+ const addr1 = await covenantAddress(redeem1, NETWORK);
167
+ const first = buildSettle(sdk, ids, { seq: -1, sompi: 0 }, claim0, opened.hex, utxo0, addr1, true);
168
+ const id0 = (await rpc.submitTransaction({ transaction: first.tx, allowOrphan: false })).transactionId;
169
+ const utxo1 = await awaitUtxo(rpc, addr1, first.outValue);
170
+ if (!utxo1)
171
+ throw new Error('the seq-0 continuation never appeared');
172
+ console.log(`\n 1. SETTLED seq 0 ${id0}`);
173
+ if (PROFILE.binds)
174
+ console.log(` covenant ${first.binding} ${covenantIdOf(first.tx)}`);
175
+ // ---- 2. THE STALE CLOSE. Consensus must refuse this: seq 0 is not > pendingSeq 0.
176
+ const stale = buildSettle(sdk, ids, { seq: 0, sompi: claim0.cumulativeSompi }, claim0, redeem1, utxo1, addr1, false);
177
+ console.log(` 2. REFUSED seq 0 again -- ${await expectRefusal(rpc, stale.tx)}`);
178
+ // ---- 3. supersede with a strictly higher seq
179
+ const claim1 = {
180
+ seq: 1, cumulativeUnits: CHUNK * 2, cumulativeSompi: CHUNK * 2 * PRICE,
181
+ prevState: digestHex(first.state),
182
+ };
183
+ const redeem2 = withState(opened.hex, claim1.seq, claim1.cumulativeSompi, spanEnd);
184
+ const addr2 = await covenantAddress(redeem2, NETWORK);
185
+ const second = buildSettle(sdk, ids, { seq: 0, sompi: claim0.cumulativeSompi }, claim1, redeem1, utxo1, addr2, true);
186
+ const id1 = (await rpc.submitTransaction({ transaction: second.tx, allowOrphan: false })).transactionId;
187
+ const utxo2 = await awaitUtxo(rpc, addr2, second.outValue);
188
+ if (!utxo2)
189
+ throw new Error('the seq-1 continuation never appeared');
190
+ console.log(` 3. SETTLED seq 1 ${id1}`);
191
+ if (PROFILE.binds) {
192
+ console.log(` covenant ${second.binding} ${covenantIdOf(second.tx)}`);
193
+ assertInherited(second.binding, second.tx, first.tx);
194
+ }
195
+ console.log(` ${Number(second.outValue) / 1e8} TKAS at the seq-1 covenant, state advanced twice`);
196
+ // ---- 4. CLOSE IT. The branch where the money actually SPLITS: the provider takes the pending
197
+ // claim, the buyer takes the remainder. Until now expire had only ever closed a session with
198
+ // NO claim posted, which is the easy half -- one output, nothing to divide.
199
+ const buyerAddr = new sdk.PrivateKey(buyer.sk).toKeypair().toAddress(networkId).toString();
200
+ const providerAddr = new sdk.PrivateKey(provider.sk).toKeypair().toAddress(networkId).toString();
201
+ const owed = BigInt(claim1.cumulativeSompi);
202
+ const refund = utxo2.amount - owed - CLOSE_FEE;
203
+ // SPEC 7.3: the window is RELATIVE, so the UTXO must actually be that old before ageDaa passes.
204
+ await awaitWindow(rpc, WINDOW);
205
+ const closeTx = buildExpire(sdk, {
206
+ buyerPk: buyer.pk, providerPk: provider.pk, signerSk: provider.sk,
207
+ redeem: redeem2, tag: opened.entries.expire?.dispatch_tag ?? '',
208
+ }, utxo2, [{ address: providerAddr, amount: owed }, { address: buyerAddr, amount: refund }], WINDOW);
209
+ const id2 = (await rpc.submitTransaction({ transaction: closeTx, allowOrphan: false })).transactionId;
210
+ const paid = await awaitUtxo(rpc, providerAddr, owed);
211
+ const refunded = await awaitUtxo(rpc, buyerAddr, refund);
212
+ if (!paid || !refunded)
213
+ throw new Error('the split did not land -- one side was not paid');
214
+ console.log(` 4. CLOSED seq 1 ${id2}`);
215
+ console.log(`\n VERIFIED provider ${Number(owed) / 1e8} TKAS, buyer ${Number(refund) / 1e8} TKAS`);
216
+ console.log(` full lifecycle on chain: settle, refuse a stale one, supersede, split\n`);
217
+ }
218
+ finally {
219
+ await rpc.disconnect().catch(() => undefined);
220
+ }
221
+ }
222
+ main().catch((err) => {
223
+ console.error(`\n ${err instanceof Error ? err.message : String(err)}\n`);
224
+ process.exit(1);
225
+ });
226
+ //# sourceMappingURL=live-settle.js.map