@totemsdk/omnia-vtxo 0.3.0 → 0.4.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/dist/anchor.d.ts CHANGED
@@ -11,6 +11,9 @@ import { type OmniaTxDraft, type StateValue } from '@totemsdk/omnia';
11
11
  import type { ChainStateProvider } from '@totemsdk/chain-provider';
12
12
  export interface BatchAnchorParams {
13
13
  operatorPublicKeyDigest: string;
14
+ ownerPublicKeyDigest: string;
15
+ /** Absolute block height after which the owner may exit unilaterally (STATE(102)). */
16
+ exitBlock: number;
14
17
  /** The epoch's Merkle root (32-byte hex). */
15
18
  root: string;
16
19
  epoch: number;
@@ -22,8 +25,8 @@ export interface BatchAnchorParams {
22
25
  amount: bigint;
23
26
  tokenId: string;
24
27
  }
25
- /** State variables carried by the pool output: STATE(100)=root, STATE(101)=epoch. */
26
- export declare function poolStateVariables(root: string, epoch: number): StateValue[];
28
+ /** Pool output state: STATE(100)=root, STATE(101)=epoch, STATE(102)=exitBlock. */
29
+ export declare function poolStateVariables(root: string, epoch: number, exitBlock: number): StateValue[];
27
30
  /** Build the batch-anchor draft (input = pool coin, output = pool state output). */
28
31
  export declare function buildBatchAnchorDraft(params: BatchAnchorParams): OmniaTxDraft;
29
32
  export declare function batchAnchorDigest(draft: OmniaTxDraft): Uint8Array;
package/dist/anchor.js CHANGED
@@ -11,24 +11,29 @@ import { computeOmniaTxDigest, omniaDraftToMinimaBytes, } from '@totemsdk/omnia'
11
11
  import { serializeTxBody, mineTxPoW, TX_POW_MIN_DIFFICULTY } from '@totemsdk/txpow';
12
12
  import { concatBytes } from '@totemsdk/core';
13
13
  import { buildAndHashPoolScript } from './pool-script.js';
14
- /** State variables carried by the pool output: STATE(100)=root, STATE(101)=epoch. */
15
- export function poolStateVariables(root, epoch) {
14
+ /** Pool output state: STATE(100)=root, STATE(101)=epoch, STATE(102)=exitBlock. */
15
+ export function poolStateVariables(root, epoch, exitBlock) {
16
16
  return [
17
17
  { port: 100, value: root, type: 'string' },
18
18
  { port: 101, value: BigInt(epoch), type: 'number' },
19
+ { port: 102, value: BigInt(exitBlock), type: 'number' },
19
20
  ];
20
21
  }
21
22
  /** Build the batch-anchor draft (input = pool coin, output = pool state output). */
22
23
  export function buildBatchAnchorDraft(params) {
23
- const { script, address } = buildAndHashPoolScript(params.operatorPublicKeyDigest);
24
+ const { script, address } = buildAndHashPoolScript({
25
+ operatorPublicKeyDigest: params.operatorPublicKeyDigest,
26
+ ownerPublicKeyDigest: params.ownerPublicKeyDigest,
27
+ exitBlock: params.exitBlock,
28
+ });
24
29
  // The input must be the operator's own pool output, not a foreign script.
25
30
  if (params.fundingScript !== script) {
26
- throw new Error('anchorBatch: fundingScript does not match the operator pool script');
31
+ throw new Error('anchorBatch: fundingScript does not match the pool script');
27
32
  }
28
33
  if (params.fundingAddress !== address) {
29
- throw new Error('anchorBatch: fundingAddress does not match the operator pool address');
34
+ throw new Error('anchorBatch: fundingAddress does not match the pool address');
30
35
  }
31
- const stateVariables = poolStateVariables(params.root, params.epoch);
36
+ const stateVariables = poolStateVariables(params.root, params.epoch, params.exitBlock);
32
37
  return {
33
38
  type: 'settlement',
34
39
  inputs: [{
@@ -11,6 +11,9 @@ import { type OmniaTxDraft, type StateValue } from '@totemsdk/omnia';
11
11
  import type { ChainStateProvider } from '@totemsdk/chain-provider';
12
12
  export interface BatchAnchorParams {
13
13
  operatorPublicKeyDigest: string;
14
+ ownerPublicKeyDigest: string;
15
+ /** Absolute block height after which the owner may exit unilaterally (STATE(102)). */
16
+ exitBlock: number;
14
17
  /** The epoch's Merkle root (32-byte hex). */
15
18
  root: string;
16
19
  epoch: number;
@@ -22,8 +25,8 @@ export interface BatchAnchorParams {
22
25
  amount: bigint;
23
26
  tokenId: string;
24
27
  }
25
- /** State variables carried by the pool output: STATE(100)=root, STATE(101)=epoch. */
26
- export declare function poolStateVariables(root: string, epoch: number): StateValue[];
28
+ /** Pool output state: STATE(100)=root, STATE(101)=epoch, STATE(102)=exitBlock. */
29
+ export declare function poolStateVariables(root: string, epoch: number, exitBlock: number): StateValue[];
27
30
  /** Build the batch-anchor draft (input = pool coin, output = pool state output). */
28
31
  export declare function buildBatchAnchorDraft(params: BatchAnchorParams): OmniaTxDraft;
29
32
  export declare function batchAnchorDigest(draft: OmniaTxDraft): Uint8Array;
@@ -18,24 +18,29 @@ const omnia_1 = require("@totemsdk/omnia");
18
18
  const txpow_1 = require("@totemsdk/txpow");
19
19
  const core_1 = require("@totemsdk/core");
20
20
  const pool_script_js_1 = require("./pool-script.js");
21
- /** State variables carried by the pool output: STATE(100)=root, STATE(101)=epoch. */
22
- function poolStateVariables(root, epoch) {
21
+ /** Pool output state: STATE(100)=root, STATE(101)=epoch, STATE(102)=exitBlock. */
22
+ function poolStateVariables(root, epoch, exitBlock) {
23
23
  return [
24
24
  { port: 100, value: root, type: 'string' },
25
25
  { port: 101, value: BigInt(epoch), type: 'number' },
26
+ { port: 102, value: BigInt(exitBlock), type: 'number' },
26
27
  ];
27
28
  }
28
29
  /** Build the batch-anchor draft (input = pool coin, output = pool state output). */
29
30
  function buildBatchAnchorDraft(params) {
30
- const { script, address } = (0, pool_script_js_1.buildAndHashPoolScript)(params.operatorPublicKeyDigest);
31
+ const { script, address } = (0, pool_script_js_1.buildAndHashPoolScript)({
32
+ operatorPublicKeyDigest: params.operatorPublicKeyDigest,
33
+ ownerPublicKeyDigest: params.ownerPublicKeyDigest,
34
+ exitBlock: params.exitBlock,
35
+ });
31
36
  // The input must be the operator's own pool output, not a foreign script.
32
37
  if (params.fundingScript !== script) {
33
- throw new Error('anchorBatch: fundingScript does not match the operator pool script');
38
+ throw new Error('anchorBatch: fundingScript does not match the pool script');
34
39
  }
35
40
  if (params.fundingAddress !== address) {
36
- throw new Error('anchorBatch: fundingAddress does not match the operator pool address');
41
+ throw new Error('anchorBatch: fundingAddress does not match the pool address');
37
42
  }
38
- const stateVariables = poolStateVariables(params.root, params.epoch);
43
+ const stateVariables = poolStateVariables(params.root, params.epoch, params.exitBlock);
39
44
  return {
40
45
  type: 'settlement',
41
46
  inputs: [{
@@ -1,20 +1,22 @@
1
1
  /**
2
- * VTXO exit transaction (RFC-038 V2).
2
+ * VTXO exit transaction (RFC-038 V2 — owner-unilateral).
3
3
  *
4
- * Builds a real exit transaction: spend the pool's on-chain coin and pay the
5
- * VTXO's owner, carrying the exit delay in STATE(102). The digest is signed
4
+ * The exit spends the pool's on-chain coin and pays the VTXO's owner, carrying
5
+ * the exit delay in STATE(110). It is signed with the **owner's** key (not the
6
+ * operator's): the pool script's `IF @BLOCK GT EXITBLOCK THEN RETURN
7
+ * SIGNEDBY(OWNER)` branch authorizes it after the exit window, so the owner can
8
+ * broadcast it without any operator signature or liveness. The digest is signed
6
9
  * (injected signer) and, when a chain provider is supplied, mined + broadcast
7
- * (injected miner). This replaces the `'mock-exit'` draft with a concrete tx.
10
+ * (injected miner).
8
11
  *
9
- * NOTE (RFC-038 §3.4): the full owner-unilateral covenant (a timelock branch the
10
- * owner can exercise without the operator, or a pre-signed exit tx) is a
11
- * follow-on; this builds and broadcasts the transaction with the delay encoded.
12
+ * The caller must pass the owner signer; the operator cannot construct a valid
13
+ * exit witness for itself.
12
14
  */
13
15
  import { type OmniaTxDraft } from '@totemsdk/omnia';
14
16
  import type { ChainStateProvider } from '@totemsdk/chain-provider';
15
17
  import type { OmniaVtxo } from './types.js';
16
- /** State port carrying the exit delay. */
17
- export declare const EXIT_DELAY_PORT = 102;
18
+ /** State port carrying the exit delay on the owner's output. */
19
+ export declare const EXIT_DELAY_PORT = 110;
18
20
  export interface ExitTxParams {
19
21
  vtxo: OmniaVtxo;
20
22
  /** The pool's on-chain coin (input). */
@@ -1,15 +1,17 @@
1
1
  "use strict";
2
2
  /**
3
- * VTXO exit transaction (RFC-038 V2).
3
+ * VTXO exit transaction (RFC-038 V2 — owner-unilateral).
4
4
  *
5
- * Builds a real exit transaction: spend the pool's on-chain coin and pay the
6
- * VTXO's owner, carrying the exit delay in STATE(102). The digest is signed
5
+ * The exit spends the pool's on-chain coin and pays the VTXO's owner, carrying
6
+ * the exit delay in STATE(110). It is signed with the **owner's** key (not the
7
+ * operator's): the pool script's `IF @BLOCK GT EXITBLOCK THEN RETURN
8
+ * SIGNEDBY(OWNER)` branch authorizes it after the exit window, so the owner can
9
+ * broadcast it without any operator signature or liveness. The digest is signed
7
10
  * (injected signer) and, when a chain provider is supplied, mined + broadcast
8
- * (injected miner). This replaces the `'mock-exit'` draft with a concrete tx.
11
+ * (injected miner).
9
12
  *
10
- * NOTE (RFC-038 §3.4): the full owner-unilateral covenant (a timelock branch the
11
- * owner can exercise without the operator, or a pre-signed exit tx) is a
12
- * follow-on; this builds and broadcasts the transaction with the delay encoded.
13
+ * The caller must pass the owner signer; the operator cannot construct a valid
14
+ * exit witness for itself.
13
15
  */
14
16
  Object.defineProperty(exports, "__esModule", { value: true });
15
17
  exports.EXIT_DELAY_PORT = void 0;
@@ -20,8 +22,8 @@ exports.buildExitTransaction = buildExitTransaction;
20
22
  const omnia_1 = require("@totemsdk/omnia");
21
23
  const txpow_1 = require("@totemsdk/txpow");
22
24
  const core_1 = require("@totemsdk/core");
23
- /** State port carrying the exit delay. */
24
- exports.EXIT_DELAY_PORT = 102;
25
+ /** State port carrying the exit delay on the owner's output. */
26
+ exports.EXIT_DELAY_PORT = 110;
25
27
  /** Draft that spends the pool coin to the VTXO owner with the exit delay encoded. */
26
28
  function buildExitTransactionDraft(params) {
27
29
  const stateVariables = [
@@ -1,19 +1,36 @@
1
1
  /**
2
- * VTXO pool on-chain script (RFC-038 V1).
2
+ * VTXO pool on-chain script (RFC-038 V2 — non-custodial).
3
3
  *
4
- * The pool is an on-chain state output holding `totalCapacity`; an epoch's batch
5
- * root is committed as STATE(100) with the epoch number in STATE(101), and the
6
- * update requires a strictly-greater epoch (`ASSERT EPOCH GT PREVSTATE(101)`) so a
7
- * old root cannot be re-anchored (RFC-038 §5 "stale/rolled-back root").
4
+ * The pool is an on-chain state output holding `totalCapacity`. An epoch's batch
5
+ * root is committed as STATE(100), the epoch number in STATE(101), and the
6
+ * owner's absolute exit-eligibility block in STATE(102).
8
7
  *
9
- * CUSTODY NOTE: this script is currently **operator-only** (`SIGNEDBY(operator)`),
10
- * so until V2 lands the owner-unilateral timelock branch the pool output is
11
- * custodied by the operator on-chain. This is surfaced in RFC-038 §3.3/§5.
8
+ * Custody (RFC-044 / CUSTODY-POLICY §2):
9
+ * - **Unilateral owner exit.** After `@BLOCK GT EXITBLOCK` the owner alone may
10
+ * spend (`RETURN SIGNEDBY(OWNER)`) — no operator signature or liveness. This
11
+ * is exercised by the pre-signed exit tx (`exit-tx.ts`).
12
+ * - **Cooperative update.** Before the exit window the pool may only move by
13
+ * `MULTISIG(2 OPERATOR OWNER)`, epoch-monotone (`ASSERT EPOCH GT
14
+ * PREVSTATE(101)`), with the exit block immutable (`ASSERT SAMESTATE(102
15
+ * 102)`) so the operator can never push the owner's escape clock, and the
16
+ * value confined to the same script address (`ASSERT VERIFYOUT(@INPUT
17
+ * @ADDRESS @AMOUNT @TOKENID TRUE)`) so the operator can never pay itself.
18
+ *
19
+ * The operator holds no unilateral path: it cannot move funds, cannot advance
20
+ * the exit block, and cannot stop the owner's exit. It is a liveness-only peer.
12
21
  */
13
- /** Operator-controlled, epoch-monotone state-carrying pool script. */
14
- export declare function buildPoolScript(operatorPublicKeyDigest: string): string;
22
+ export interface PoolScriptParams {
23
+ /** Operator WOTS public-key digest (64 hex). */
24
+ operatorPublicKeyDigest: string;
25
+ /** Pool owner WOTS public-key digest (64 hex). */
26
+ ownerPublicKeyDigest: string;
27
+ /** Absolute block height after which the owner may exit unilaterally. */
28
+ exitBlock: number;
29
+ }
30
+ /** Non-custodial, epoch-monotone, owner-escapable state-carrying pool script. */
31
+ export declare function buildPoolScript(params: PoolScriptParams): string;
15
32
  export declare function poolScriptAddress(script: string): string;
16
- export declare function buildAndHashPoolScript(operatorPublicKeyDigest: string): {
33
+ export declare function buildAndHashPoolScript(params: PoolScriptParams): {
17
34
  script: string;
18
35
  address: string;
19
36
  };
@@ -1,41 +1,60 @@
1
1
  "use strict";
2
2
  /**
3
- * VTXO pool on-chain script (RFC-038 V1).
3
+ * VTXO pool on-chain script (RFC-038 V2 — non-custodial).
4
4
  *
5
- * The pool is an on-chain state output holding `totalCapacity`; an epoch's batch
6
- * root is committed as STATE(100) with the epoch number in STATE(101), and the
7
- * update requires a strictly-greater epoch (`ASSERT EPOCH GT PREVSTATE(101)`) so a
8
- * old root cannot be re-anchored (RFC-038 §5 "stale/rolled-back root").
5
+ * The pool is an on-chain state output holding `totalCapacity`. An epoch's batch
6
+ * root is committed as STATE(100), the epoch number in STATE(101), and the
7
+ * owner's absolute exit-eligibility block in STATE(102).
9
8
  *
10
- * CUSTODY NOTE: this script is currently **operator-only** (`SIGNEDBY(operator)`),
11
- * so until V2 lands the owner-unilateral timelock branch the pool output is
12
- * custodied by the operator on-chain. This is surfaced in RFC-038 §3.3/§5.
9
+ * Custody (RFC-044 / CUSTODY-POLICY §2):
10
+ * - **Unilateral owner exit.** After `@BLOCK GT EXITBLOCK` the owner alone may
11
+ * spend (`RETURN SIGNEDBY(OWNER)`) — no operator signature or liveness. This
12
+ * is exercised by the pre-signed exit tx (`exit-tx.ts`).
13
+ * - **Cooperative update.** Before the exit window the pool may only move by
14
+ * `MULTISIG(2 OPERATOR OWNER)`, epoch-monotone (`ASSERT EPOCH GT
15
+ * PREVSTATE(101)`), with the exit block immutable (`ASSERT SAMESTATE(102
16
+ * 102)`) so the operator can never push the owner's escape clock, and the
17
+ * value confined to the same script address (`ASSERT VERIFYOUT(@INPUT
18
+ * @ADDRESS @AMOUNT @TOKENID TRUE)`) so the operator can never pay itself.
19
+ *
20
+ * The operator holds no unilateral path: it cannot move funds, cannot advance
21
+ * the exit block, and cannot stop the owner's exit. It is a liveness-only peer.
13
22
  */
14
23
  Object.defineProperty(exports, "__esModule", { value: true });
15
24
  exports.buildPoolScript = buildPoolScript;
16
25
  exports.poolScriptAddress = poolScriptAddress;
17
26
  exports.buildAndHashPoolScript = buildAndHashPoolScript;
18
27
  const core_1 = require("@totemsdk/core");
19
- function operatorPkHex(operatorPublicKeyDigest) {
20
- const raw = operatorPublicKeyDigest.replace(/^0x/i, '');
28
+ function pkHex(digest, label) {
29
+ const raw = digest.replace(/^0x/i, '');
21
30
  if (!/^[0-9a-fA-F]{64}$/.test(raw)) {
22
- throw new Error(`operator public key digest must be 64 hex chars, got ${JSON.stringify(operatorPublicKeyDigest)}`);
31
+ throw new Error(`${label} public key digest must be 64 hex chars, got ${JSON.stringify(digest)}`);
23
32
  }
24
33
  return '0X' + raw.toUpperCase();
25
34
  }
26
- /** Operator-controlled, epoch-monotone state-carrying pool script. */
27
- function buildPoolScript(operatorPublicKeyDigest) {
35
+ /** Non-custodial, epoch-monotone, owner-escapable state-carrying pool script. */
36
+ function buildPoolScript(params) {
37
+ const owner = pkHex(params.ownerPublicKeyDigest, 'owner');
38
+ const operator = pkHex(params.operatorPublicKeyDigest, 'operator');
39
+ if (!Number.isInteger(params.exitBlock) || params.exitBlock < 0) {
40
+ throw new Error(`exitBlock must be a non-negative integer, got ${JSON.stringify(params.exitBlock)}`);
41
+ }
28
42
  return [
29
- 'LET ROOT=STATE(100)',
30
43
  'LET EPOCH=STATE(101)',
44
+ 'LET EXITBLOCK=STATE(102)',
45
+ 'IF @BLOCK GT EXITBLOCK THEN',
46
+ ` RETURN SIGNEDBY(${owner})`,
47
+ 'ENDIF',
31
48
  'ASSERT EPOCH GT PREVSTATE(101)',
32
- `RETURN SIGNEDBY(${operatorPkHex(operatorPublicKeyDigest)})`,
49
+ 'ASSERT SAMESTATE(102 102)',
50
+ 'ASSERT VERIFYOUT(@INPUT @ADDRESS @AMOUNT @TOKENID TRUE)',
51
+ `RETURN MULTISIG(2 ${operator} ${owner})`,
33
52
  ].join('\n');
34
53
  }
35
54
  function poolScriptAddress(script) {
36
55
  return (0, core_1.computeScriptAddress)(script);
37
56
  }
38
- function buildAndHashPoolScript(operatorPublicKeyDigest) {
39
- const script = buildPoolScript(operatorPublicKeyDigest);
57
+ function buildAndHashPoolScript(params) {
58
+ const script = buildPoolScript(params);
40
59
  return { script, address: poolScriptAddress(script) };
41
60
  }
package/dist/exit-tx.d.ts CHANGED
@@ -1,20 +1,22 @@
1
1
  /**
2
- * VTXO exit transaction (RFC-038 V2).
2
+ * VTXO exit transaction (RFC-038 V2 — owner-unilateral).
3
3
  *
4
- * Builds a real exit transaction: spend the pool's on-chain coin and pay the
5
- * VTXO's owner, carrying the exit delay in STATE(102). The digest is signed
4
+ * The exit spends the pool's on-chain coin and pays the VTXO's owner, carrying
5
+ * the exit delay in STATE(110). It is signed with the **owner's** key (not the
6
+ * operator's): the pool script's `IF @BLOCK GT EXITBLOCK THEN RETURN
7
+ * SIGNEDBY(OWNER)` branch authorizes it after the exit window, so the owner can
8
+ * broadcast it without any operator signature or liveness. The digest is signed
6
9
  * (injected signer) and, when a chain provider is supplied, mined + broadcast
7
- * (injected miner). This replaces the `'mock-exit'` draft with a concrete tx.
10
+ * (injected miner).
8
11
  *
9
- * NOTE (RFC-038 §3.4): the full owner-unilateral covenant (a timelock branch the
10
- * owner can exercise without the operator, or a pre-signed exit tx) is a
11
- * follow-on; this builds and broadcasts the transaction with the delay encoded.
12
+ * The caller must pass the owner signer; the operator cannot construct a valid
13
+ * exit witness for itself.
12
14
  */
13
15
  import { type OmniaTxDraft } from '@totemsdk/omnia';
14
16
  import type { ChainStateProvider } from '@totemsdk/chain-provider';
15
17
  import type { OmniaVtxo } from './types.js';
16
- /** State port carrying the exit delay. */
17
- export declare const EXIT_DELAY_PORT = 102;
18
+ /** State port carrying the exit delay on the owner's output. */
19
+ export declare const EXIT_DELAY_PORT = 110;
18
20
  export interface ExitTxParams {
19
21
  vtxo: OmniaVtxo;
20
22
  /** The pool's on-chain coin (input). */
package/dist/exit-tx.js CHANGED
@@ -1,20 +1,22 @@
1
1
  /**
2
- * VTXO exit transaction (RFC-038 V2).
2
+ * VTXO exit transaction (RFC-038 V2 — owner-unilateral).
3
3
  *
4
- * Builds a real exit transaction: spend the pool's on-chain coin and pay the
5
- * VTXO's owner, carrying the exit delay in STATE(102). The digest is signed
4
+ * The exit spends the pool's on-chain coin and pays the VTXO's owner, carrying
5
+ * the exit delay in STATE(110). It is signed with the **owner's** key (not the
6
+ * operator's): the pool script's `IF @BLOCK GT EXITBLOCK THEN RETURN
7
+ * SIGNEDBY(OWNER)` branch authorizes it after the exit window, so the owner can
8
+ * broadcast it without any operator signature or liveness. The digest is signed
6
9
  * (injected signer) and, when a chain provider is supplied, mined + broadcast
7
- * (injected miner). This replaces the `'mock-exit'` draft with a concrete tx.
10
+ * (injected miner).
8
11
  *
9
- * NOTE (RFC-038 §3.4): the full owner-unilateral covenant (a timelock branch the
10
- * owner can exercise without the operator, or a pre-signed exit tx) is a
11
- * follow-on; this builds and broadcasts the transaction with the delay encoded.
12
+ * The caller must pass the owner signer; the operator cannot construct a valid
13
+ * exit witness for itself.
12
14
  */
13
15
  import { computeOmniaTxDigest, omniaDraftToMinimaBytes, serializeTxDraft, } from '@totemsdk/omnia';
14
16
  import { serializeTxBody, mineTxPoW, TX_POW_MIN_DIFFICULTY } from '@totemsdk/txpow';
15
17
  import { concatBytes } from '@totemsdk/core';
16
- /** State port carrying the exit delay. */
17
- export const EXIT_DELAY_PORT = 102;
18
+ /** State port carrying the exit delay on the owner's output. */
19
+ export const EXIT_DELAY_PORT = 110;
18
20
  /** Draft that spends the pool coin to the VTXO owner with the exit delay encoded. */
19
21
  export function buildExitTransactionDraft(params) {
20
22
  const stateVariables = [
@@ -1,19 +1,36 @@
1
1
  /**
2
- * VTXO pool on-chain script (RFC-038 V1).
2
+ * VTXO pool on-chain script (RFC-038 V2 — non-custodial).
3
3
  *
4
- * The pool is an on-chain state output holding `totalCapacity`; an epoch's batch
5
- * root is committed as STATE(100) with the epoch number in STATE(101), and the
6
- * update requires a strictly-greater epoch (`ASSERT EPOCH GT PREVSTATE(101)`) so a
7
- * old root cannot be re-anchored (RFC-038 §5 "stale/rolled-back root").
4
+ * The pool is an on-chain state output holding `totalCapacity`. An epoch's batch
5
+ * root is committed as STATE(100), the epoch number in STATE(101), and the
6
+ * owner's absolute exit-eligibility block in STATE(102).
8
7
  *
9
- * CUSTODY NOTE: this script is currently **operator-only** (`SIGNEDBY(operator)`),
10
- * so until V2 lands the owner-unilateral timelock branch the pool output is
11
- * custodied by the operator on-chain. This is surfaced in RFC-038 §3.3/§5.
8
+ * Custody (RFC-044 / CUSTODY-POLICY §2):
9
+ * - **Unilateral owner exit.** After `@BLOCK GT EXITBLOCK` the owner alone may
10
+ * spend (`RETURN SIGNEDBY(OWNER)`) — no operator signature or liveness. This
11
+ * is exercised by the pre-signed exit tx (`exit-tx.ts`).
12
+ * - **Cooperative update.** Before the exit window the pool may only move by
13
+ * `MULTISIG(2 OPERATOR OWNER)`, epoch-monotone (`ASSERT EPOCH GT
14
+ * PREVSTATE(101)`), with the exit block immutable (`ASSERT SAMESTATE(102
15
+ * 102)`) so the operator can never push the owner's escape clock, and the
16
+ * value confined to the same script address (`ASSERT VERIFYOUT(@INPUT
17
+ * @ADDRESS @AMOUNT @TOKENID TRUE)`) so the operator can never pay itself.
18
+ *
19
+ * The operator holds no unilateral path: it cannot move funds, cannot advance
20
+ * the exit block, and cannot stop the owner's exit. It is a liveness-only peer.
12
21
  */
13
- /** Operator-controlled, epoch-monotone state-carrying pool script. */
14
- export declare function buildPoolScript(operatorPublicKeyDigest: string): string;
22
+ export interface PoolScriptParams {
23
+ /** Operator WOTS public-key digest (64 hex). */
24
+ operatorPublicKeyDigest: string;
25
+ /** Pool owner WOTS public-key digest (64 hex). */
26
+ ownerPublicKeyDigest: string;
27
+ /** Absolute block height after which the owner may exit unilaterally. */
28
+ exitBlock: number;
29
+ }
30
+ /** Non-custodial, epoch-monotone, owner-escapable state-carrying pool script. */
31
+ export declare function buildPoolScript(params: PoolScriptParams): string;
15
32
  export declare function poolScriptAddress(script: string): string;
16
- export declare function buildAndHashPoolScript(operatorPublicKeyDigest: string): {
33
+ export declare function buildAndHashPoolScript(params: PoolScriptParams): {
17
34
  script: string;
18
35
  address: string;
19
36
  };
@@ -1,36 +1,55 @@
1
1
  /**
2
- * VTXO pool on-chain script (RFC-038 V1).
2
+ * VTXO pool on-chain script (RFC-038 V2 — non-custodial).
3
3
  *
4
- * The pool is an on-chain state output holding `totalCapacity`; an epoch's batch
5
- * root is committed as STATE(100) with the epoch number in STATE(101), and the
6
- * update requires a strictly-greater epoch (`ASSERT EPOCH GT PREVSTATE(101)`) so a
7
- * old root cannot be re-anchored (RFC-038 §5 "stale/rolled-back root").
4
+ * The pool is an on-chain state output holding `totalCapacity`. An epoch's batch
5
+ * root is committed as STATE(100), the epoch number in STATE(101), and the
6
+ * owner's absolute exit-eligibility block in STATE(102).
8
7
  *
9
- * CUSTODY NOTE: this script is currently **operator-only** (`SIGNEDBY(operator)`),
10
- * so until V2 lands the owner-unilateral timelock branch the pool output is
11
- * custodied by the operator on-chain. This is surfaced in RFC-038 §3.3/§5.
8
+ * Custody (RFC-044 / CUSTODY-POLICY §2):
9
+ * - **Unilateral owner exit.** After `@BLOCK GT EXITBLOCK` the owner alone may
10
+ * spend (`RETURN SIGNEDBY(OWNER)`) — no operator signature or liveness. This
11
+ * is exercised by the pre-signed exit tx (`exit-tx.ts`).
12
+ * - **Cooperative update.** Before the exit window the pool may only move by
13
+ * `MULTISIG(2 OPERATOR OWNER)`, epoch-monotone (`ASSERT EPOCH GT
14
+ * PREVSTATE(101)`), with the exit block immutable (`ASSERT SAMESTATE(102
15
+ * 102)`) so the operator can never push the owner's escape clock, and the
16
+ * value confined to the same script address (`ASSERT VERIFYOUT(@INPUT
17
+ * @ADDRESS @AMOUNT @TOKENID TRUE)`) so the operator can never pay itself.
18
+ *
19
+ * The operator holds no unilateral path: it cannot move funds, cannot advance
20
+ * the exit block, and cannot stop the owner's exit. It is a liveness-only peer.
12
21
  */
13
22
  import { computeScriptAddress } from '@totemsdk/core';
14
- function operatorPkHex(operatorPublicKeyDigest) {
15
- const raw = operatorPublicKeyDigest.replace(/^0x/i, '');
23
+ function pkHex(digest, label) {
24
+ const raw = digest.replace(/^0x/i, '');
16
25
  if (!/^[0-9a-fA-F]{64}$/.test(raw)) {
17
- throw new Error(`operator public key digest must be 64 hex chars, got ${JSON.stringify(operatorPublicKeyDigest)}`);
26
+ throw new Error(`${label} public key digest must be 64 hex chars, got ${JSON.stringify(digest)}`);
18
27
  }
19
28
  return '0X' + raw.toUpperCase();
20
29
  }
21
- /** Operator-controlled, epoch-monotone state-carrying pool script. */
22
- export function buildPoolScript(operatorPublicKeyDigest) {
30
+ /** Non-custodial, epoch-monotone, owner-escapable state-carrying pool script. */
31
+ export function buildPoolScript(params) {
32
+ const owner = pkHex(params.ownerPublicKeyDigest, 'owner');
33
+ const operator = pkHex(params.operatorPublicKeyDigest, 'operator');
34
+ if (!Number.isInteger(params.exitBlock) || params.exitBlock < 0) {
35
+ throw new Error(`exitBlock must be a non-negative integer, got ${JSON.stringify(params.exitBlock)}`);
36
+ }
23
37
  return [
24
- 'LET ROOT=STATE(100)',
25
38
  'LET EPOCH=STATE(101)',
39
+ 'LET EXITBLOCK=STATE(102)',
40
+ 'IF @BLOCK GT EXITBLOCK THEN',
41
+ ` RETURN SIGNEDBY(${owner})`,
42
+ 'ENDIF',
26
43
  'ASSERT EPOCH GT PREVSTATE(101)',
27
- `RETURN SIGNEDBY(${operatorPkHex(operatorPublicKeyDigest)})`,
44
+ 'ASSERT SAMESTATE(102 102)',
45
+ 'ASSERT VERIFYOUT(@INPUT @ADDRESS @AMOUNT @TOKENID TRUE)',
46
+ `RETURN MULTISIG(2 ${operator} ${owner})`,
28
47
  ].join('\n');
29
48
  }
30
49
  export function poolScriptAddress(script) {
31
50
  return computeScriptAddress(script);
32
51
  }
33
- export function buildAndHashPoolScript(operatorPublicKeyDigest) {
34
- const script = buildPoolScript(operatorPublicKeyDigest);
52
+ export function buildAndHashPoolScript(params) {
53
+ const script = buildPoolScript(params);
35
54
  return { script, address: poolScriptAddress(script) };
36
55
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@totemsdk/omnia-vtxo",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Virtual UTXO / payment-pool claim layer for Omnia — cash-like off-chain balance primitive for Totem Edge",
5
5
  "main": "dist/cjs/index.js",
6
6
  "types": "dist/cjs/index.d.ts",
@@ -25,11 +25,11 @@
25
25
  ],
26
26
  "dependencies": {
27
27
  "@totemsdk/chain-provider": "^0.2.1",
28
- "@totemsdk/omnia": "^1.0.3",
29
- "@totemsdk/core": "^1.2.11",
28
+ "@totemsdk/omnia": "^2.0.0",
30
29
  "@totemsdk/storage": "^0.1.0",
31
- "@totemsdk/wots-lease": "^1.0.1",
32
- "@totemsdk/txpow": "^1.0.2"
30
+ "@totemsdk/txpow": "^1.0.2",
31
+ "@totemsdk/core": "^1.2.11",
32
+ "@totemsdk/wots-lease": "^1.0.1"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/jest": "^30.0.0",