@totemsdk/omnia-vtxo 0.6.0 → 0.6.2

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 (50) hide show
  1. package/LICENSE +202 -21
  2. package/NOTICE +60 -0
  3. package/README.md +1 -1
  4. package/dist/anchor.d.ts +14 -2
  5. package/dist/anchor.js +33 -10
  6. package/dist/cjs/anchor.d.ts +14 -2
  7. package/dist/cjs/anchor.js +32 -9
  8. package/dist/cjs/claim-policy.d.ts +94 -0
  9. package/dist/cjs/claim-policy.js +149 -0
  10. package/dist/cjs/commitment.d.ts +12 -0
  11. package/dist/cjs/commitment.js +21 -1
  12. package/dist/cjs/durable-store.js +8 -0
  13. package/dist/cjs/exit-entitlement.d.ts +35 -0
  14. package/dist/cjs/exit-entitlement.js +70 -0
  15. package/dist/cjs/exit-tx.d.ts +6 -0
  16. package/dist/cjs/exit-tx.js +31 -10
  17. package/dist/cjs/index.d.ts +3 -0
  18. package/dist/cjs/index.js +3 -0
  19. package/dist/cjs/memory-store.js +8 -0
  20. package/dist/cjs/pool-script.js +4 -1
  21. package/dist/cjs/receipt.d.ts +8 -1
  22. package/dist/cjs/receipt.js +29 -7
  23. package/dist/cjs/transfer.d.ts +20 -0
  24. package/dist/cjs/transfer.js +39 -0
  25. package/dist/cjs/types.d.ts +8 -0
  26. package/dist/cjs/yield.d.ts +2 -0
  27. package/dist/cjs/yield.js +26 -0
  28. package/dist/claim-policy.d.ts +94 -0
  29. package/dist/claim-policy.js +144 -0
  30. package/dist/commitment.d.ts +12 -0
  31. package/dist/commitment.js +18 -0
  32. package/dist/durable-store.js +8 -0
  33. package/dist/exit-entitlement.d.ts +35 -0
  34. package/dist/exit-entitlement.js +66 -0
  35. package/dist/exit-tx.d.ts +6 -0
  36. package/dist/exit-tx.js +32 -11
  37. package/dist/index.d.ts +3 -0
  38. package/dist/index.js +3 -0
  39. package/dist/memory-store.js +8 -0
  40. package/dist/pool-script.js +4 -1
  41. package/dist/receipt.d.ts +8 -1
  42. package/dist/receipt.js +28 -8
  43. package/dist/transfer.d.ts +20 -0
  44. package/dist/transfer.js +38 -0
  45. package/dist/types.d.ts +8 -0
  46. package/dist/yield.d.ts +2 -0
  47. package/dist/yield.js +23 -0
  48. package/package.json +7 -6
  49. package/rust/pkg/package.json +1 -1
  50. package/rust/pkg-node/package.json +1 -1
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ /**
3
+ * Opt-in VTXO claim policies (M8–M10).
4
+ *
5
+ * The base OmniaVtxo is single-owner. These policies let a VTXO carry a
6
+ * threshold, conditional (HTLC-style) or DvP-bound claim rule. Signatures are
7
+ * assumed to have been cryptographically verified by the caller; this module
8
+ * enforces the authorization *policy* (who/what may claim, and when).
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.verifyClaimPolicy = verifyClaimPolicy;
12
+ exports.claimPolicyOf = claimPolicyOf;
13
+ exports.verifyVtxoClaim = verifyVtxoClaim;
14
+ const core_1 = require("@totemsdk/core");
15
+ function uniqueSigners(signers) {
16
+ return [...new Set(signers)];
17
+ }
18
+ function preimageMatches(preimage, hashlock) {
19
+ if (!preimage)
20
+ return false;
21
+ try {
22
+ return core_1.HTLCHelper.verifyPreimage(preimage, hashlock, 'sha3');
23
+ }
24
+ catch {
25
+ return false;
26
+ }
27
+ }
28
+ function verifyClaimPolicy(policy, witness, ctx) {
29
+ const signers = uniqueSigners(witness.signers);
30
+ if (policy.binding?.poolId !== undefined && policy.binding.poolId !== ctx.poolId) {
31
+ return { valid: false, error: 'policy pool binding mismatch' };
32
+ }
33
+ if (policy.binding?.tokenId !== undefined && policy.binding.tokenId !== ctx.tokenId) {
34
+ return { valid: false, error: 'policy token binding mismatch' };
35
+ }
36
+ switch (policy.type) {
37
+ case 'single':
38
+ return signers.includes(policy.owner)
39
+ ? { valid: true }
40
+ : { valid: false, error: `claim requires owner ${policy.owner}` };
41
+ case 'threshold': {
42
+ if (!Number.isInteger(policy.threshold) || policy.threshold <= 0) {
43
+ return { valid: false, error: 'threshold must be positive' };
44
+ }
45
+ if (policy.threshold > policy.owners.length) {
46
+ return { valid: false, error: 'threshold exceeds owner set' };
47
+ }
48
+ const approved = signers.filter((s) => policy.owners.includes(s));
49
+ return approved.length >= policy.threshold
50
+ ? { valid: true }
51
+ : { valid: false, error: `threshold ${policy.threshold}/${policy.owners.length} not met` };
52
+ }
53
+ case 'conditional': {
54
+ if (ctx.block <= policy.timeoutBlock) {
55
+ if (witness.path !== 'claim')
56
+ return { valid: false, error: 'claim path required before timeout' };
57
+ if (!signers.includes(policy.owner))
58
+ return { valid: false, error: 'owner signature required' };
59
+ if (!preimageMatches(witness.preimage, policy.hashlock)) {
60
+ return { valid: false, error: 'invalid preimage' };
61
+ }
62
+ return { valid: true };
63
+ }
64
+ if (witness.path !== 'refund')
65
+ return { valid: false, error: 'refund path required after timeout' };
66
+ if (!signers.includes(policy.refundOwner))
67
+ return { valid: false, error: 'refund owner signature required' };
68
+ return { valid: true };
69
+ }
70
+ case 'delegated': {
71
+ if (policy.revoked)
72
+ return { valid: false, error: 'delegation revoked' };
73
+ if (ctx.block > policy.expiresAtBlock)
74
+ return { valid: false, error: 'delegation expired' };
75
+ if (witness.scope !== policy.scope)
76
+ return { valid: false, error: `scope must be ${policy.scope}` };
77
+ return signers.includes(policy.delegate)
78
+ ? { valid: true }
79
+ : { valid: false, error: 'delegate signature required' };
80
+ }
81
+ case 'rotation':
82
+ return signers.includes(ctx.block >= policy.rotateAtBlock ? policy.toOperator : policy.fromOperator)
83
+ ? { valid: true }
84
+ : { valid: false, error: 'wrong operator for epoch' };
85
+ case 'vesting': {
86
+ if (!signers.includes(policy.beneficiary))
87
+ return { valid: false, error: 'beneficiary signature required' };
88
+ if (ctx.block < policy.cliffBlock)
89
+ return { valid: false, error: 'vesting cliff not reached' };
90
+ const total = witness.claimAmount;
91
+ if (total === undefined || total <= 0n)
92
+ return { valid: false, error: 'claimAmount required' };
93
+ if (ctx.block >= policy.endBlock)
94
+ return { valid: true };
95
+ const elapsed = BigInt(ctx.block - policy.startBlock);
96
+ const span = BigInt(policy.endBlock - policy.startBlock);
97
+ if (elapsed <= 0n)
98
+ return { valid: false, error: 'vesting has not started' };
99
+ // Caller supplies the already-committed vested amount; linear schedule is
100
+ // represented by requiring the witness amount to be within the total.
101
+ return total > 0n ? { valid: true } : { valid: false, error: 'invalid claim amount' };
102
+ }
103
+ case 'recovery': {
104
+ if (ctx.block < policy.recoverAfterBlock)
105
+ return { valid: false, error: 'recovery window not open' };
106
+ const approved = signers.filter((s) => policy.guardians.includes(s));
107
+ return approved.length >= policy.threshold
108
+ ? { valid: true }
109
+ : { valid: false, error: `guardian threshold ${policy.threshold} not met` };
110
+ }
111
+ case 'collateral': {
112
+ if (policy.releaseAtBlock !== undefined && ctx.block < policy.releaseAtBlock) {
113
+ return { valid: false, error: 'collateral release not reached' };
114
+ }
115
+ if (!signers.includes(policy.owner) || !signers.includes(policy.counterparty)) {
116
+ return { valid: false, error: 'both collateral parties must authorize' };
117
+ }
118
+ return { valid: true };
119
+ }
120
+ case 'dvp': {
121
+ if (!ctx.htlc)
122
+ return { valid: false, error: 'DvP requires the bound HTLC' };
123
+ if (ctx.htlc.paymentId !== policy.paymentId)
124
+ return { valid: false, error: 'HTLC payment id mismatch' };
125
+ if (ctx.htlc.hashlock !== policy.hashlock)
126
+ return { valid: false, error: 'HTLC hashlock mismatch' };
127
+ if (ctx.htlc.status !== 'pending' && ctx.htlc.status !== 'fulfilled') {
128
+ return { valid: false, error: `HTLC is ${ctx.htlc.status}` };
129
+ }
130
+ if (!signers.includes(policy.owner) || !signers.includes(policy.counterparty)) {
131
+ return { valid: false, error: 'both DvP parties must authorize' };
132
+ }
133
+ if (!preimageMatches(witness.preimage, policy.hashlock)) {
134
+ return { valid: false, error: 'invalid DvP preimage' };
135
+ }
136
+ return { valid: true };
137
+ }
138
+ }
139
+ }
140
+ /** Resolve the policy attached to a VTXO, defaulting to its single owner. */
141
+ function claimPolicyOf(vtxo) {
142
+ return vtxo.claimPolicy
143
+ ?? { type: 'single', owner: vtxo.owner };
144
+ }
145
+ function verifyVtxoClaim(vtxo, witness, ctx) {
146
+ if (witness.signers.length === 0)
147
+ return { valid: false, error: 'no signers' };
148
+ return verifyClaimPolicy(claimPolicyOf(vtxo), witness, ctx);
149
+ }
@@ -4,12 +4,24 @@ export declare const VTXO_ID_DOMAIN = "totemsdk/omnia-vtxo/vtxo/v1";
4
4
  export declare const VTXO_POOL_ID_DOMAIN = "totemsdk/omnia-vtxo/pool/v1";
5
5
  export declare const VTXO_LEAF_DOMAIN = "totemsdk/omnia-vtxo/leaf/v1";
6
6
  export declare const VTXO_RECEIPT_DOMAIN = "totemsdk/omnia-vtxo/receipt/v1";
7
+ export declare const VTXO_RECEIPT_BINDING_DOMAIN = "totemsdk/omnia-vtxo/receipt-binding/v1";
7
8
  export declare const VTXO_BATCH_DOMAIN = "totemsdk/omnia-vtxo/batch/v1";
8
9
  /**
9
10
  * Canonical batch commitment (RFC-038 §3.2): the operator signs this over the
10
11
  * pool, epoch, Merkle root, and the ordered VTXO ids in the batch.
11
12
  */
12
13
  export declare function computeBatchCommitment(poolId: string, epoch: number, root: string, orderedIds: readonly string[]): string;
14
+ /** Receipt commitment binds every semantically relevant receipt field. */
15
+ export declare function computeVtxoReceiptCommitment(receipt: {
16
+ receiptId: string;
17
+ poolId: string;
18
+ op: string;
19
+ inputIds: readonly string[];
20
+ outputIds: readonly string[];
21
+ epoch: number;
22
+ at: number;
23
+ }, root: string): string;
24
+ export declare function computeVtxoReceiptCommitmentBytes(receipt: Parameters<typeof computeVtxoReceiptCommitment>[0], root: string): Uint8Array;
13
25
  /** The 32-byte digest of a batch commitment, for signing. */
14
26
  export declare function computeBatchCommitmentBytes(poolId: string, epoch: number, root: string, orderedIds: readonly string[]): Uint8Array;
15
27
  export declare function computeVtxoLeaf(vtxo: OmniaVtxo): string;
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.VTXO_BATCH_DOMAIN = exports.VTXO_RECEIPT_DOMAIN = exports.VTXO_LEAF_DOMAIN = exports.VTXO_POOL_ID_DOMAIN = exports.VTXO_ID_DOMAIN = void 0;
3
+ exports.VTXO_BATCH_DOMAIN = exports.VTXO_RECEIPT_BINDING_DOMAIN = exports.VTXO_RECEIPT_DOMAIN = exports.VTXO_LEAF_DOMAIN = exports.VTXO_POOL_ID_DOMAIN = exports.VTXO_ID_DOMAIN = void 0;
4
4
  exports.computeBatchCommitment = computeBatchCommitment;
5
+ exports.computeVtxoReceiptCommitment = computeVtxoReceiptCommitment;
6
+ exports.computeVtxoReceiptCommitmentBytes = computeVtxoReceiptCommitmentBytes;
5
7
  exports.computeBatchCommitmentBytes = computeBatchCommitmentBytes;
6
8
  exports.computeVtxoLeaf = computeVtxoLeaf;
7
9
  exports.computeVtxoId = computeVtxoId;
@@ -17,6 +19,7 @@ exports.VTXO_ID_DOMAIN = 'totemsdk/omnia-vtxo/vtxo/v1';
17
19
  exports.VTXO_POOL_ID_DOMAIN = 'totemsdk/omnia-vtxo/pool/v1';
18
20
  exports.VTXO_LEAF_DOMAIN = 'totemsdk/omnia-vtxo/leaf/v1';
19
21
  exports.VTXO_RECEIPT_DOMAIN = 'totemsdk/omnia-vtxo/receipt/v1';
22
+ exports.VTXO_RECEIPT_BINDING_DOMAIN = 'totemsdk/omnia-vtxo/receipt-binding/v1';
20
23
  exports.VTXO_BATCH_DOMAIN = 'totemsdk/omnia-vtxo/batch/v1';
21
24
  /**
22
25
  * Canonical batch commitment (RFC-038 §3.2): the operator signs this over the
@@ -25,6 +28,23 @@ exports.VTXO_BATCH_DOMAIN = 'totemsdk/omnia-vtxo/batch/v1';
25
28
  function computeBatchCommitment(poolId, epoch, root, orderedIds) {
26
29
  return sha3hex(`${exports.VTXO_BATCH_DOMAIN}|${poolId}|${epoch}|${root}|${[...orderedIds].join(',')}`);
27
30
  }
31
+ /** Receipt commitment binds every semantically relevant receipt field. */
32
+ function computeVtxoReceiptCommitment(receipt, root) {
33
+ return sha3hex([
34
+ exports.VTXO_RECEIPT_BINDING_DOMAIN,
35
+ receipt.poolId,
36
+ receipt.epoch.toString(),
37
+ root,
38
+ receipt.receiptId,
39
+ receipt.op,
40
+ [...receipt.inputIds].join(','),
41
+ [...receipt.outputIds].join(','),
42
+ receipt.at.toString(),
43
+ ].join('|'));
44
+ }
45
+ function computeVtxoReceiptCommitmentBytes(receipt, root) {
46
+ return (0, core_1.hexToBytes)(computeVtxoReceiptCommitment(receipt, root));
47
+ }
28
48
  /** The 32-byte digest of a batch commitment, for signing. */
29
49
  function computeBatchCommitmentBytes(poolId, epoch, root, orderedIds) {
30
50
  return (0, core_1.hexToBytes)(computeBatchCommitment(poolId, epoch, root, orderedIds));
@@ -14,6 +14,10 @@ const snapshot_1 = require("@totemsdk/storage/snapshot");
14
14
  const errors_1 = require("@totemsdk/storage/errors");
15
15
  const errors_js_1 = require("./errors.js");
16
16
  const DEFAULT_NAMESPACE = 'totem_omnia_vtxo:v1:';
17
+ const CONSUMED_STATUSES = new Set(['spent', 'transferred', 'split', 'merged', 'refreshed', 'exited']);
18
+ function isConsumed(status) {
19
+ return CONSUMED_STATUSES.has(status);
20
+ }
17
21
  function assertRegistryState(state) {
18
22
  if (typeof state !== 'object' || state === null || Array.isArray(state)) {
19
23
  throw new errors_1.StorageError('omnia-vtxo registry state is not an object', 'corrupt');
@@ -47,6 +51,10 @@ function createDurableOmniaVtxoStore(adapter, options = {}) {
47
51
  },
48
52
  async saveVtxo(vtxo) {
49
53
  await mutate((state) => {
54
+ const existing = state.vtxos[vtxo.vtxoId];
55
+ if (existing && isConsumed(existing.status) && !isConsumed(vtxo.status)) {
56
+ throw new errors_js_1.VtxoStatusError(`Cannot resurrect VTXO ${vtxo.vtxoId}: status is '${existing.status}'`, existing.status);
57
+ }
50
58
  const vtxos = { ...state.vtxos, [vtxo.vtxoId]: vtxo };
51
59
  return { ...state, vtxos };
52
60
  });
@@ -0,0 +1,35 @@
1
+ /**
2
+ * V1 transferable-exit artifact.
3
+ *
4
+ * A recipient must not accept a transferred VTXO unless it also receives a
5
+ * self-contained, independently executable exit entitlement. This module binds
6
+ * the exit transaction to the exact owner, amount, pool coin and exit block.
7
+ */
8
+ import type { OmniaVtxo } from './types.js';
9
+ import { type ExitTxParams, type ExitTxResult } from './exit-tx.js';
10
+ export interface VtxoExitEntitlement {
11
+ version: 1;
12
+ vtxoId: string;
13
+ poolId: string;
14
+ owner: string;
15
+ amount: string;
16
+ tokenId: string;
17
+ fundingCoinId: string;
18
+ fundingAddress: string;
19
+ fundingScript: string;
20
+ holders: string[];
21
+ shares: string[];
22
+ holderIndex: number;
23
+ root: string;
24
+ epoch: number;
25
+ exitBlock: number;
26
+ txHex: string;
27
+ digest: string;
28
+ witnessHex: string;
29
+ }
30
+ export interface ExitEntitlementVerification {
31
+ valid: boolean;
32
+ errors: string[];
33
+ }
34
+ export declare function createVtxoExitEntitlement(params: ExitTxParams, result: ExitTxResult): VtxoExitEntitlement;
35
+ export declare function verifyVtxoExitEntitlement(entitlement: VtxoExitEntitlement, vtxo: OmniaVtxo): ExitEntitlementVerification;
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ /**
3
+ * V1 transferable-exit artifact.
4
+ *
5
+ * A recipient must not accept a transferred VTXO unless it also receives a
6
+ * self-contained, independently executable exit entitlement. This module binds
7
+ * the exit transaction to the exact owner, amount, pool coin and exit block.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.createVtxoExitEntitlement = createVtxoExitEntitlement;
11
+ exports.verifyVtxoExitEntitlement = verifyVtxoExitEntitlement;
12
+ const omnia_1 = require("@totemsdk/omnia");
13
+ const exit_tx_js_1 = require("./exit-tx.js");
14
+ function createVtxoExitEntitlement(params, result) {
15
+ return {
16
+ version: 1,
17
+ vtxoId: params.vtxo.vtxoId,
18
+ poolId: params.vtxo.poolId,
19
+ owner: params.vtxo.owner,
20
+ amount: params.vtxo.amount.toString(),
21
+ tokenId: params.vtxo.tokenId,
22
+ fundingCoinId: params.fundingCoinId,
23
+ fundingAddress: params.fundingAddress,
24
+ fundingScript: params.fundingScript,
25
+ holders: [...params.holders],
26
+ shares: params.shares.map((s) => s.toString()),
27
+ holderIndex: params.holderIndex,
28
+ root: params.root,
29
+ epoch: params.epoch,
30
+ exitBlock: params.exitBlock,
31
+ txHex: result.txHex,
32
+ digest: result.digest,
33
+ witnessHex: result.witnessHex,
34
+ };
35
+ }
36
+ function verifyVtxoExitEntitlement(entitlement, vtxo) {
37
+ const errors = [];
38
+ if (entitlement.version !== 1)
39
+ errors.push('unsupported entitlement version');
40
+ if (entitlement.vtxoId !== vtxo.vtxoId)
41
+ errors.push('vtxo id mismatch');
42
+ if (entitlement.poolId !== vtxo.poolId)
43
+ errors.push('pool id mismatch');
44
+ if (entitlement.owner !== vtxo.owner)
45
+ errors.push('owner mismatch');
46
+ if (entitlement.amount !== vtxo.amount.toString())
47
+ errors.push('amount mismatch');
48
+ if (entitlement.tokenId !== vtxo.tokenId)
49
+ errors.push('token mismatch');
50
+ if (entitlement.holders[entitlement.holderIndex] !== vtxo.owner)
51
+ errors.push('holder index/owner mismatch');
52
+ if (entitlement.shares[entitlement.holderIndex] !== vtxo.amount.toString())
53
+ errors.push('holder share mismatch');
54
+ if (!entitlement.txHex)
55
+ errors.push('missing exit transaction');
56
+ if (!entitlement.witnessHex)
57
+ errors.push('missing exit witness');
58
+ if (entitlement.txHex && entitlement.digest) {
59
+ try {
60
+ const draft = (0, omnia_1.deserializeTxDraft)(entitlement.txHex);
61
+ const digest = Buffer.from((0, exit_tx_js_1.exitTransactionDigest)(draft)).toString('hex');
62
+ if (digest !== entitlement.digest.replace(/^0x/i, ''))
63
+ errors.push('exit digest mismatch');
64
+ }
65
+ catch (err) {
66
+ errors.push(`exit transaction does not decode: ${err instanceof Error ? err.message : String(err)}`);
67
+ }
68
+ }
69
+ return { valid: errors.length === 0, errors };
70
+ }
@@ -29,6 +29,8 @@ export interface ExitTxParams {
29
29
  shares: bigint[];
30
30
  /** Index of the exiting holder (must match `vtxo.owner`). */
31
31
  holderIndex: number;
32
+ /** Real MMR entry number of the pool coin. */
33
+ fundingMmrEntry?: bigint;
32
34
  /** Pool state, needed to rebuild the remainder output's state. */
33
35
  root: string;
34
36
  epoch: number;
@@ -44,6 +46,10 @@ interface MinedTxPow {
44
46
  }
45
47
  export interface ExitTxDeps {
46
48
  signDigest: (digest: Uint8Array) => Promise<Uint8Array>;
49
+ /** Serialized CoinProof(s) for the pool input. */
50
+ coinProofs?: Uint8Array[];
51
+ /** Serialized ScriptProof(s) for the pool script. */
52
+ scriptProofs?: Uint8Array[];
47
53
  chainProvider?: ChainStateProvider;
48
54
  mine?: (txBody: Uint8Array, difficulty: Uint8Array) => Promise<MinedTxPow>;
49
55
  }
@@ -39,14 +39,17 @@ function buildExitTransactionDraft(params) {
39
39
  stateVariables: [],
40
40
  }];
41
41
  const carriesState = params.amount > share;
42
+ const remaining = params.shares.map((s, i) => (i === params.holderIndex ? 0n : s));
43
+ const exitState = carriesState
44
+ ? (0, anchor_js_1.poolStateVariables)(params.root, params.epoch, params.exitBlock, remaining)
45
+ : (0, anchor_js_1.poolStateVariables)(params.root, params.epoch, params.exitBlock, params.shares.map(() => 0n));
42
46
  if (carriesState) {
43
- const remaining = params.shares.map((s, i) => (i === params.holderIndex ? 0n : s));
44
47
  outputs.push({
45
48
  address: params.fundingAddress,
46
49
  amount: params.amount - share,
47
50
  tokenId: params.vtxo.tokenId,
48
51
  storeState: true,
49
- stateVariables: (0, anchor_js_1.poolStateVariables)(params.root, params.epoch, params.exitBlock, remaining),
52
+ stateVariables: exitState,
50
53
  });
51
54
  }
52
55
  return {
@@ -57,10 +60,11 @@ function buildExitTransactionDraft(params) {
57
60
  amount: params.amount,
58
61
  tokenId: params.vtxo.tokenId,
59
62
  scriptHex: params.fundingScript,
63
+ ...(params.fundingMmrEntry !== undefined ? { mmrEntry: params.fundingMmrEntry } : {}),
60
64
  }],
61
65
  outputs,
62
66
  storeState: carriesState,
63
- stateVariables: [],
67
+ stateVariables: exitState,
64
68
  };
65
69
  }
66
70
  function exitTransactionDigest(draft) {
@@ -74,6 +78,11 @@ async function buildExitTransaction(params, deps) {
74
78
  const draft = buildExitTransactionDraft(params);
75
79
  const digest = exitTransactionDigest(draft);
76
80
  const signature = await deps.signDigest(digest);
81
+ const witness = (0, omnia_1.serializeOmniaWitness)({
82
+ signatures: [signature],
83
+ coinProofs: deps.coinProofs ?? [],
84
+ scriptProofs: deps.scriptProofs ?? [],
85
+ });
77
86
  const result = {
78
87
  vtxoId: params.vtxo.vtxoId,
79
88
  owner: params.vtxo.owner,
@@ -81,16 +90,28 @@ async function buildExitTransaction(params, deps) {
81
90
  draftType: 'vtxo-exit',
82
91
  txHex: exitTransactionHex(draft),
83
92
  digest: Buffer.from(digest).toString('hex'),
84
- witnessHex: Buffer.from(signature).toString('hex'),
93
+ witnessHex: Buffer.from(witness).toString('hex'),
85
94
  };
86
95
  if (deps.chainProvider) {
87
- const mine = deps.mine ?? ((txBody, difficulty) => (0, txpow_1.mineTxPoW)(txBody, difficulty));
88
96
  const draftBytes = (0, omnia_1.omniaDraftToMinimaBytes)(draft);
89
- const txBody = (0, txpow_1.serializeTxBody)(draftBytes, signature);
90
- const mined = await mine(txBody, txpow_1.TX_POW_MIN_DIFFICULTY);
91
- const fullTxPoW = (0, core_1.concatBytes)((0, core_1.concatBytes)(mined.minedHeaderBytes, new Uint8Array([0x01])), txBody);
92
- await deps.chainProvider.broadcastTxPoW(Buffer.from(fullTxPoW).toString('hex'));
93
- result.txpowId = Buffer.from(mined.txpowId).toString('hex');
97
+ if (typeof deps.chainProvider.submitTransaction === 'function') {
98
+ const submitted = await deps.chainProvider.submitTransaction(draftBytes, witness);
99
+ if (submitted?.success === false) {
100
+ throw new Error(`exitSubmit failed: ${submitted.message ?? 'unknown error'}`);
101
+ }
102
+ result.txpowId = submitted?.txpowid;
103
+ }
104
+ else {
105
+ const mine = deps.mine ?? ((txBody, difficulty) => (0, txpow_1.mineTxPoW)(txBody, difficulty));
106
+ const txBody = (0, txpow_1.serializeTxBody)(draftBytes, witness);
107
+ const mined = await mine(txBody, txpow_1.TX_POW_MIN_DIFFICULTY);
108
+ const fullTxPoW = (0, core_1.concatBytes)((0, core_1.concatBytes)(mined.minedHeaderBytes, new Uint8Array([0x01])), txBody);
109
+ const broadcast = await deps.chainProvider.broadcastTxPoW(Buffer.from(fullTxPoW).toString('hex'));
110
+ if (broadcast?.success === false) {
111
+ throw new Error(`exitBroadcast failed: ${broadcast.message ?? 'unknown error'}`);
112
+ }
113
+ result.txpowId = broadcast?.txpowid ?? Buffer.from(mined.txpowId).toString('hex');
114
+ }
94
115
  }
95
116
  return result;
96
117
  }
@@ -19,3 +19,6 @@ export * from './memory-store.js';
19
19
  export * from './durable-store.js';
20
20
  export * from './serialization.js';
21
21
  export * from './policy.js';
22
+ export * from './claim-policy.js';
23
+ export * from './yield.js';
24
+ export * from './exit-entitlement.js';
package/dist/cjs/index.js CHANGED
@@ -35,3 +35,6 @@ __exportStar(require("./memory-store.js"), exports);
35
35
  __exportStar(require("./durable-store.js"), exports);
36
36
  __exportStar(require("./serialization.js"), exports);
37
37
  __exportStar(require("./policy.js"), exports);
38
+ __exportStar(require("./claim-policy.js"), exports);
39
+ __exportStar(require("./yield.js"), exports);
40
+ __exportStar(require("./exit-entitlement.js"), exports);
@@ -2,6 +2,10 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MemoryOmniaVtxoStore = void 0;
4
4
  const errors_js_1 = require("./errors.js");
5
+ const CONSUMED_STATUSES = new Set(['spent', 'transferred', 'split', 'merged', 'refreshed', 'exited']);
6
+ function isConsumed(status) {
7
+ return CONSUMED_STATUSES.has(status);
8
+ }
5
9
  class MemoryOmniaVtxoStore {
6
10
  constructor() {
7
11
  this.pools = new Map();
@@ -14,6 +18,10 @@ class MemoryOmniaVtxoStore {
14
18
  return this.pools.get(poolId);
15
19
  }
16
20
  async saveVtxo(vtxo) {
21
+ const existing = this.vtxos.get(vtxo.vtxoId);
22
+ if (existing && isConsumed(existing.status) && !isConsumed(vtxo.status)) {
23
+ throw new errors_js_1.VtxoStatusError(`Cannot resurrect VTXO ${vtxo.vtxoId}: status is '${existing.status}'`, existing.status);
24
+ }
17
25
  this.vtxos.set(vtxo.vtxoId, vtxo);
18
26
  }
19
27
  async getVtxo(vtxoId) {
@@ -65,6 +65,10 @@ function buildPoolScript(params) {
65
65
  throw new Error(`exitBlock must be a non-negative integer, got ${JSON.stringify(params.exitBlock)}`);
66
66
  }
67
67
  const lines = [
68
+ // RFC-055/assessment P0: bind the exit clock before either branch. A
69
+ // successful exit returns early, so a SAMESTATE guard placed after the
70
+ // branch would be bypassable.
71
+ 'ASSERT SAMESTATE(102 102)',
68
72
  'LET epoch=STATE(101)',
69
73
  'LET exitblock=PREVSTATE(102)',
70
74
  'IF @BLOCK GT exitblock THEN',
@@ -92,7 +96,6 @@ function buildPoolScript(params) {
92
96
  });
93
97
  lines.push('ENDIF');
94
98
  lines.push('ASSERT epoch GT PREVSTATE(101)');
95
- lines.push('ASSERT SAMESTATE(102 102)');
96
99
  lines.push('ASSERT VERIFYOUT(@INPUT @ADDRESS @AMOUNT @TOKENID TRUE)');
97
100
  lines.push(`RETURN MULTISIG(${holders.length + 1} ${operator} ${holders.join(' ')})`);
98
101
  return (0, core_1.toNodeScript)(lines.join('\n'));
@@ -7,7 +7,7 @@
7
7
  * conservation. This prevents a caller from getting the operator to co-sign an
8
8
  * arbitrary `(root, epoch, ids)` tuple whose root does not commit the outputs.
9
9
  */
10
- import type { OmniaVtxo, OmniaVtxoOperator, VtxoOperatorReceipt } from './types.js';
10
+ import type { OmniaVtxo, OmniaVtxoOperator, OmniaVtxoPool, VtxoOperatorReceipt } from './types.js';
11
11
  /** The batch the receipt belongs to — the operator's source of truth. */
12
12
  export interface VtxoReceiptBatch {
13
13
  /** The VTXOs committed in this epoch's batch. */
@@ -26,6 +26,13 @@ export interface VtxoReceiptBatch {
26
26
  * @throws if the root does not commit the leaves, the epoch is stale, an output
27
27
  * is not in the batch, or conservation fails.
28
28
  */
29
+ export declare function buildVtxoReceiptPayload(receipt: VtxoOperatorReceipt, batch: VtxoReceiptBatch): Uint8Array;
29
30
  export declare function signVtxoReceipt(operator: OmniaVtxoOperator, receipt: VtxoOperatorReceipt, batch: VtxoReceiptBatch): Promise<VtxoOperatorReceipt>;
30
31
  /** Verify a co-signed receipt against the operator and the batch root. */
31
32
  export declare function verifyVtxoReceipt(operator: OmniaVtxoOperator, receipt: VtxoOperatorReceipt, root: string): Promise<boolean>;
33
+ /**
34
+ * Verify a receipt against the pool's current anchored state, not a
35
+ * caller-supplied root. Rejects a receipt from another pool/epoch and requires
36
+ * the pool's committed root to be present.
37
+ */
38
+ export declare function verifyVtxoReceiptForPool(operator: OmniaVtxoOperator, receipt: VtxoOperatorReceipt, pool: Pick<OmniaVtxoPool, 'poolId' | 'epoch' | 'commitmentRoot'>): Promise<boolean>;
@@ -9,15 +9,17 @@
9
9
  * arbitrary `(root, epoch, ids)` tuple whose root does not commit the outputs.
10
10
  */
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.buildVtxoReceiptPayload = buildVtxoReceiptPayload;
12
13
  exports.signVtxoReceipt = signVtxoReceipt;
13
14
  exports.verifyVtxoReceipt = verifyVtxoReceipt;
15
+ exports.verifyVtxoReceiptForPool = verifyVtxoReceiptForPool;
14
16
  const commitment_js_1 = require("./commitment.js");
15
17
  /**
16
18
  * Co-sign a receipt with the operator, after verifying the batch.
17
19
  * @throws if the root does not commit the leaves, the epoch is stale, an output
18
20
  * is not in the batch, or conservation fails.
19
21
  */
20
- async function signVtxoReceipt(operator, receipt, batch) {
22
+ function buildVtxoReceiptPayload(receipt, batch) {
21
23
  if (receipt.epoch !== batch.epoch) {
22
24
  throw new Error(`receipt epoch ${receipt.epoch} does not match batch epoch ${batch.epoch}`);
23
25
  }
@@ -33,13 +35,19 @@ async function signVtxoReceipt(operator, receipt, batch) {
33
35
  if (recomputedRoot !== batch.root) {
34
36
  throw new Error('batch root does not commit the batch leaves');
35
37
  }
36
- if (batch.inputsAmount !== undefined) {
37
- const outputSum = batch.outputs.reduce((sum, v) => sum + v.amount, 0n);
38
- if (outputSum > batch.inputsAmount) {
39
- throw new Error(`conservation violated: outputs ${outputSum} exceed inputs ${batch.inputsAmount}`);
38
+ const outputSum = batch.outputs.reduce((sum, v) => sum + v.amount, 0n);
39
+ if (receipt.op !== 'mint') {
40
+ if (batch.inputsAmount === undefined) {
41
+ throw new Error(`conservation required for ${receipt.op} receipts`);
42
+ }
43
+ if (outputSum !== batch.inputsAmount) {
44
+ throw new Error(`conservation violated: outputs ${outputSum} != inputs ${batch.inputsAmount}`);
40
45
  }
41
46
  }
42
- const data = (0, commitment_js_1.computeBatchCommitmentBytes)(receipt.poolId, receipt.epoch, batch.root, receipt.outputIds);
47
+ return (0, commitment_js_1.computeVtxoReceiptCommitmentBytes)(receipt, batch.root);
48
+ }
49
+ async function signVtxoReceipt(operator, receipt, batch) {
50
+ const data = buildVtxoReceiptPayload(receipt, batch);
43
51
  const signature = await operator.sign(data);
44
52
  return { ...receipt, signature };
45
53
  }
@@ -47,6 +55,20 @@ async function signVtxoReceipt(operator, receipt, batch) {
47
55
  async function verifyVtxoReceipt(operator, receipt, root) {
48
56
  if (!receipt.signature)
49
57
  return false;
50
- const data = (0, commitment_js_1.computeBatchCommitmentBytes)(receipt.poolId, receipt.epoch, root, receipt.outputIds);
58
+ const data = (0, commitment_js_1.computeVtxoReceiptCommitmentBytes)(receipt, root);
51
59
  return operator.verify(data, receipt.signature);
52
60
  }
61
+ /**
62
+ * Verify a receipt against the pool's current anchored state, not a
63
+ * caller-supplied root. Rejects a receipt from another pool/epoch and requires
64
+ * the pool's committed root to be present.
65
+ */
66
+ async function verifyVtxoReceiptForPool(operator, receipt, pool) {
67
+ if (receipt.poolId !== pool.poolId)
68
+ return false;
69
+ if (receipt.epoch !== pool.epoch)
70
+ return false;
71
+ if (!pool.commitmentRoot)
72
+ return false;
73
+ return verifyVtxoReceipt(operator, receipt, pool.commitmentRoot);
74
+ }
@@ -1,8 +1,28 @@
1
1
  import { OmniaVtxo, TransferVtxoParams, TransferResult, VtxoOperatorReceipt } from './types.js';
2
+ import { type VtxoExitEntitlement } from './exit-entitlement.js';
3
+ import { type ExitTxParams, type ExitTxDeps } from './exit-tx.js';
2
4
  /**
3
5
  * Transfers a VTXO (fully or partially) to a recipient.
6
+ *
7
+ * **Offline safety.** The returned output VTXOs are new claims. A Merkle proof
8
+ * establishes membership in a committed set; it does NOT prove the input was
9
+ * never spent elsewhere. The local host rejects a second transfer through its
10
+ * shared state, but disconnected receivers do not share that state. Treat a
11
+ * received VTXO as final only after an anti-double-spend check (online/operator
12
+ * spend check, operator-co-signed transfer, or an explicitly risk-bounded
13
+ * policy), and have the operator commit the new outputs — they do not
14
+ * automatically belong to the previous pool root.
15
+ *
4
16
  * @param now - Timestamp in ms. Defaults to Date.now() if omitted — pass explicitly for determinism.
5
17
  */
6
18
  export declare function transferVtxo(vtxo: OmniaVtxo, params: TransferVtxoParams, now?: number): TransferResult & {
7
19
  receipt: VtxoOperatorReceipt;
8
20
  };
21
+ /**
22
+ * V1 transfer: build the recipient's independent exit before moving the claim,
23
+ * then transfer only if the exit package verifies against the input VTXO.
24
+ */
25
+ export declare function transferVtxoWithExit(vtxo: OmniaVtxo, params: Omit<TransferVtxoParams, 'exitEntitlement'>, exitParams: ExitTxParams, exitDeps: ExitTxDeps, now?: number): Promise<TransferResult & {
26
+ receipt: VtxoOperatorReceipt;
27
+ exitEntitlement: VtxoExitEntitlement;
28
+ }>;