@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
@@ -1,10 +1,23 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.transferVtxo = transferVtxo;
4
+ exports.transferVtxoWithExit = transferVtxoWithExit;
4
5
  const errors_js_1 = require("./errors.js");
5
6
  const commitment_js_1 = require("./commitment.js");
7
+ const exit_entitlement_js_1 = require("./exit-entitlement.js");
8
+ const exit_tx_js_1 = require("./exit-tx.js");
6
9
  /**
7
10
  * Transfers a VTXO (fully or partially) to a recipient.
11
+ *
12
+ * **Offline safety.** The returned output VTXOs are new claims. A Merkle proof
13
+ * establishes membership in a committed set; it does NOT prove the input was
14
+ * never spent elsewhere. The local host rejects a second transfer through its
15
+ * shared state, but disconnected receivers do not share that state. Treat a
16
+ * received VTXO as final only after an anti-double-spend check (online/operator
17
+ * spend check, operator-co-signed transfer, or an explicitly risk-bounded
18
+ * policy), and have the operator commit the new outputs — they do not
19
+ * automatically belong to the previous pool root.
20
+ *
8
21
  * @param now - Timestamp in ms. Defaults to Date.now() if omitted — pass explicitly for determinism.
9
22
  */
10
23
  function transferVtxo(vtxo, params, now) {
@@ -18,6 +31,15 @@ function transferVtxo(vtxo, params, now) {
18
31
  if (params.amount > vtxo.amount) {
19
32
  throw new errors_js_1.VtxoAmountError(`Transfer amount ${params.amount} exceeds VTXO amount ${vtxo.amount}`);
20
33
  }
34
+ if (params.exitEntitlement) {
35
+ if (params.amount !== vtxo.amount) {
36
+ throw new errors_js_1.VtxoAmountError('exit entitlement requires a full-value transfer');
37
+ }
38
+ const check = (0, exit_entitlement_js_1.verifyVtxoExitEntitlement)(params.exitEntitlement, vtxo);
39
+ if (!check.valid) {
40
+ throw new errors_js_1.VtxoAmountError(`invalid exit entitlement: ${check.errors.join('; ')}`);
41
+ }
42
+ }
21
43
  const isPartial = params.amount < vtxo.amount;
22
44
  const changeAmount = vtxo.amount - params.amount;
23
45
  const outputId = (0, commitment_js_1.computeVtxoId)({
@@ -62,6 +84,7 @@ function transferVtxo(vtxo, params, now) {
62
84
  tokenId: vtxo.tokenId,
63
85
  status: 'active',
64
86
  epoch: vtxo.epoch,
87
+ ...(params.exitEntitlement ? { exitEntitlement: params.exitEntitlement } : {}),
65
88
  proof: {
66
89
  leaf: '',
67
90
  root: '',
@@ -150,3 +173,19 @@ function transferVtxo(vtxo, params, now) {
150
173
  },
151
174
  };
152
175
  }
176
+ /**
177
+ * V1 transfer: build the recipient's independent exit before moving the claim,
178
+ * then transfer only if the exit package verifies against the input VTXO.
179
+ */
180
+ async function transferVtxoWithExit(vtxo, params, exitParams, exitDeps, now) {
181
+ if (exitParams.vtxo.vtxoId !== vtxo.vtxoId) {
182
+ throw new errors_js_1.VtxoAmountError('exit entitlement VTXO does not match transfer input');
183
+ }
184
+ if (params.amount !== vtxo.amount) {
185
+ throw new errors_js_1.VtxoAmountError('transferVtxoWithExit requires a full-value transfer');
186
+ }
187
+ const exit = await (0, exit_tx_js_1.buildExitTransaction)(exitParams, exitDeps);
188
+ const entitlement = (0, exit_entitlement_js_1.createVtxoExitEntitlement)(exitParams, exit);
189
+ const transferred = transferVtxo(vtxo, { ...params, exitEntitlement: entitlement }, now);
190
+ return { ...transferred, exitEntitlement: entitlement };
191
+ }
@@ -1,3 +1,5 @@
1
+ import type { VtxoClaimPolicy } from './claim-policy.js';
2
+ import type { VtxoExitEntitlement } from './exit-entitlement.js';
1
3
  export type VtxoId = string;
2
4
  export type VtxoStatus = 'active' | 'transferred' | 'split' | 'merged' | 'refreshed' | 'exiting' | 'exited' | 'spent' | 'expired' | 'invalid';
3
5
  export type VtxoOp = 'mint' | 'transfer' | 'split' | 'merge' | 'refresh' | 'exit_initiated' | 'exit_complete' | 'spent';
@@ -50,6 +52,10 @@ export interface OmniaVtxo {
50
52
  expiresAt?: number;
51
53
  /** The on-chain funding proof recorded at mint (deep proof, #28). */
52
54
  fundingProof?: unknown;
55
+ /** Opt-in multiparty claim policy (M8–M18). */
56
+ claimPolicy?: VtxoClaimPolicy;
57
+ /** V1: independently executable exit package delivered to the owner. */
58
+ exitEntitlement?: VtxoExitEntitlement;
53
59
  /** Set when an exit draft consumed this VTXO — prevents double-exit. */
54
60
  exitConsumedAt?: number;
55
61
  exitReceiptId?: string;
@@ -99,6 +105,8 @@ export interface TransferVtxoParams {
99
105
  amount: bigint;
100
106
  nonce: string;
101
107
  changeNonce?: string;
108
+ /** V1: exit package the recipient must receive with the claim. */
109
+ exitEntitlement?: VtxoExitEntitlement;
102
110
  }
103
111
  export interface SplitVtxoParams {
104
112
  amounts: bigint[];
@@ -0,0 +1,2 @@
1
+ /** Deterministic pro-rata yield distribution for multiparty VTXO/pool claims. */
2
+ export declare function distributeYield(total: bigint, weights: Record<string, bigint>): Record<string, bigint>;
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.distributeYield = distributeYield;
4
+ /** Deterministic pro-rata yield distribution for multiparty VTXO/pool claims. */
5
+ function distributeYield(total, weights) {
6
+ if (total < 0n)
7
+ throw new Error('yield total must be non-negative');
8
+ const entries = Object.entries(weights).filter(([, weight]) => weight > 0n);
9
+ if (entries.length === 0)
10
+ throw new Error('yield requires positive weights');
11
+ const denominator = entries.reduce((sum, [, weight]) => sum + weight, 0n);
12
+ if (denominator <= 0n)
13
+ throw new Error('yield weight sum must be positive');
14
+ const out = {};
15
+ let allocated = 0n;
16
+ for (const [id, weight] of entries) {
17
+ const share = (total * weight) / denominator;
18
+ out[id] = share;
19
+ allocated += share;
20
+ }
21
+ // Deterministic remainder assignment to the first entry.
22
+ const remainder = total - allocated;
23
+ if (remainder > 0n)
24
+ out[entries[0][0]] += remainder;
25
+ return out;
26
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Opt-in VTXO claim policies (M8–M10).
3
+ *
4
+ * The base OmniaVtxo is single-owner. These policies let a VTXO carry a
5
+ * threshold, conditional (HTLC-style) or DvP-bound claim rule. Signatures are
6
+ * assumed to have been cryptographically verified by the caller; this module
7
+ * enforces the authorization *policy* (who/what may claim, and when).
8
+ */
9
+ import type { OmniaVtxo } from './types.js';
10
+ export interface VtxoPolicyBinding {
11
+ poolId?: string;
12
+ tokenId?: string;
13
+ }
14
+ export type VtxoClaimPolicy = ({
15
+ type: 'single';
16
+ owner: string;
17
+ } | {
18
+ type: 'threshold';
19
+ threshold: number;
20
+ owners: string[];
21
+ } | {
22
+ type: 'conditional';
23
+ owner: string;
24
+ refundOwner: string;
25
+ hashlock: string;
26
+ timeoutBlock: number;
27
+ } | {
28
+ type: 'dvp';
29
+ owner: string;
30
+ counterparty: string;
31
+ paymentId: string;
32
+ hashlock: string;
33
+ timeoutBlock: number;
34
+ } | {
35
+ type: 'delegated';
36
+ owner: string;
37
+ delegate: string;
38
+ scope: 'claim' | 'transfer';
39
+ expiresAtBlock: number;
40
+ revoked?: boolean;
41
+ } | {
42
+ type: 'rotation';
43
+ fromOperator: string;
44
+ toOperator: string;
45
+ rotateAtBlock: number;
46
+ } | {
47
+ type: 'vesting';
48
+ owner: string;
49
+ beneficiary: string;
50
+ startBlock: number;
51
+ cliffBlock: number;
52
+ endBlock: number;
53
+ } | {
54
+ type: 'recovery';
55
+ owner: string;
56
+ guardians: string[];
57
+ threshold: number;
58
+ recoverAfterBlock: number;
59
+ } | {
60
+ type: 'collateral';
61
+ owner: string;
62
+ counterparty: string;
63
+ releaseAtBlock?: number;
64
+ }) & {
65
+ binding?: VtxoPolicyBinding;
66
+ };
67
+ export interface VtxoClaimWitness {
68
+ signers: string[];
69
+ preimage?: string;
70
+ /** 'claim' (before timeout) or 'refund' (after timeout). */
71
+ path?: 'claim' | 'refund';
72
+ /** Delegated policy scope being exercised. */
73
+ scope?: 'claim' | 'transfer';
74
+ /** Vesting claim amount. */
75
+ claimAmount?: bigint;
76
+ }
77
+ export interface VtxoClaimContext {
78
+ block: number;
79
+ poolId?: string;
80
+ tokenId?: string;
81
+ htlc?: {
82
+ paymentId: string;
83
+ hashlock: string;
84
+ status: 'pending' | 'fulfilled' | 'timed_out';
85
+ };
86
+ }
87
+ export interface VtxoClaimResult {
88
+ valid: boolean;
89
+ error?: string;
90
+ }
91
+ export declare function verifyClaimPolicy(policy: VtxoClaimPolicy, witness: VtxoClaimWitness, ctx: VtxoClaimContext): VtxoClaimResult;
92
+ /** Resolve the policy attached to a VTXO, defaulting to its single owner. */
93
+ export declare function claimPolicyOf(vtxo: OmniaVtxo): VtxoClaimPolicy;
94
+ export declare function verifyVtxoClaim(vtxo: OmniaVtxo, witness: VtxoClaimWitness, ctx: VtxoClaimContext): VtxoClaimResult;
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Opt-in VTXO claim policies (M8–M10).
3
+ *
4
+ * The base OmniaVtxo is single-owner. These policies let a VTXO carry a
5
+ * threshold, conditional (HTLC-style) or DvP-bound claim rule. Signatures are
6
+ * assumed to have been cryptographically verified by the caller; this module
7
+ * enforces the authorization *policy* (who/what may claim, and when).
8
+ */
9
+ import { HTLCHelper } from '@totemsdk/core';
10
+ function uniqueSigners(signers) {
11
+ return [...new Set(signers)];
12
+ }
13
+ function preimageMatches(preimage, hashlock) {
14
+ if (!preimage)
15
+ return false;
16
+ try {
17
+ return HTLCHelper.verifyPreimage(preimage, hashlock, 'sha3');
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
23
+ export function verifyClaimPolicy(policy, witness, ctx) {
24
+ const signers = uniqueSigners(witness.signers);
25
+ if (policy.binding?.poolId !== undefined && policy.binding.poolId !== ctx.poolId) {
26
+ return { valid: false, error: 'policy pool binding mismatch' };
27
+ }
28
+ if (policy.binding?.tokenId !== undefined && policy.binding.tokenId !== ctx.tokenId) {
29
+ return { valid: false, error: 'policy token binding mismatch' };
30
+ }
31
+ switch (policy.type) {
32
+ case 'single':
33
+ return signers.includes(policy.owner)
34
+ ? { valid: true }
35
+ : { valid: false, error: `claim requires owner ${policy.owner}` };
36
+ case 'threshold': {
37
+ if (!Number.isInteger(policy.threshold) || policy.threshold <= 0) {
38
+ return { valid: false, error: 'threshold must be positive' };
39
+ }
40
+ if (policy.threshold > policy.owners.length) {
41
+ return { valid: false, error: 'threshold exceeds owner set' };
42
+ }
43
+ const approved = signers.filter((s) => policy.owners.includes(s));
44
+ return approved.length >= policy.threshold
45
+ ? { valid: true }
46
+ : { valid: false, error: `threshold ${policy.threshold}/${policy.owners.length} not met` };
47
+ }
48
+ case 'conditional': {
49
+ if (ctx.block <= policy.timeoutBlock) {
50
+ if (witness.path !== 'claim')
51
+ return { valid: false, error: 'claim path required before timeout' };
52
+ if (!signers.includes(policy.owner))
53
+ return { valid: false, error: 'owner signature required' };
54
+ if (!preimageMatches(witness.preimage, policy.hashlock)) {
55
+ return { valid: false, error: 'invalid preimage' };
56
+ }
57
+ return { valid: true };
58
+ }
59
+ if (witness.path !== 'refund')
60
+ return { valid: false, error: 'refund path required after timeout' };
61
+ if (!signers.includes(policy.refundOwner))
62
+ return { valid: false, error: 'refund owner signature required' };
63
+ return { valid: true };
64
+ }
65
+ case 'delegated': {
66
+ if (policy.revoked)
67
+ return { valid: false, error: 'delegation revoked' };
68
+ if (ctx.block > policy.expiresAtBlock)
69
+ return { valid: false, error: 'delegation expired' };
70
+ if (witness.scope !== policy.scope)
71
+ return { valid: false, error: `scope must be ${policy.scope}` };
72
+ return signers.includes(policy.delegate)
73
+ ? { valid: true }
74
+ : { valid: false, error: 'delegate signature required' };
75
+ }
76
+ case 'rotation':
77
+ return signers.includes(ctx.block >= policy.rotateAtBlock ? policy.toOperator : policy.fromOperator)
78
+ ? { valid: true }
79
+ : { valid: false, error: 'wrong operator for epoch' };
80
+ case 'vesting': {
81
+ if (!signers.includes(policy.beneficiary))
82
+ return { valid: false, error: 'beneficiary signature required' };
83
+ if (ctx.block < policy.cliffBlock)
84
+ return { valid: false, error: 'vesting cliff not reached' };
85
+ const total = witness.claimAmount;
86
+ if (total === undefined || total <= 0n)
87
+ return { valid: false, error: 'claimAmount required' };
88
+ if (ctx.block >= policy.endBlock)
89
+ return { valid: true };
90
+ const elapsed = BigInt(ctx.block - policy.startBlock);
91
+ const span = BigInt(policy.endBlock - policy.startBlock);
92
+ if (elapsed <= 0n)
93
+ return { valid: false, error: 'vesting has not started' };
94
+ // Caller supplies the already-committed vested amount; linear schedule is
95
+ // represented by requiring the witness amount to be within the total.
96
+ return total > 0n ? { valid: true } : { valid: false, error: 'invalid claim amount' };
97
+ }
98
+ case 'recovery': {
99
+ if (ctx.block < policy.recoverAfterBlock)
100
+ return { valid: false, error: 'recovery window not open' };
101
+ const approved = signers.filter((s) => policy.guardians.includes(s));
102
+ return approved.length >= policy.threshold
103
+ ? { valid: true }
104
+ : { valid: false, error: `guardian threshold ${policy.threshold} not met` };
105
+ }
106
+ case 'collateral': {
107
+ if (policy.releaseAtBlock !== undefined && ctx.block < policy.releaseAtBlock) {
108
+ return { valid: false, error: 'collateral release not reached' };
109
+ }
110
+ if (!signers.includes(policy.owner) || !signers.includes(policy.counterparty)) {
111
+ return { valid: false, error: 'both collateral parties must authorize' };
112
+ }
113
+ return { valid: true };
114
+ }
115
+ case 'dvp': {
116
+ if (!ctx.htlc)
117
+ return { valid: false, error: 'DvP requires the bound HTLC' };
118
+ if (ctx.htlc.paymentId !== policy.paymentId)
119
+ return { valid: false, error: 'HTLC payment id mismatch' };
120
+ if (ctx.htlc.hashlock !== policy.hashlock)
121
+ return { valid: false, error: 'HTLC hashlock mismatch' };
122
+ if (ctx.htlc.status !== 'pending' && ctx.htlc.status !== 'fulfilled') {
123
+ return { valid: false, error: `HTLC is ${ctx.htlc.status}` };
124
+ }
125
+ if (!signers.includes(policy.owner) || !signers.includes(policy.counterparty)) {
126
+ return { valid: false, error: 'both DvP parties must authorize' };
127
+ }
128
+ if (!preimageMatches(witness.preimage, policy.hashlock)) {
129
+ return { valid: false, error: 'invalid DvP preimage' };
130
+ }
131
+ return { valid: true };
132
+ }
133
+ }
134
+ }
135
+ /** Resolve the policy attached to a VTXO, defaulting to its single owner. */
136
+ export function claimPolicyOf(vtxo) {
137
+ return vtxo.claimPolicy
138
+ ?? { type: 'single', owner: vtxo.owner };
139
+ }
140
+ export function verifyVtxoClaim(vtxo, witness, ctx) {
141
+ if (witness.signers.length === 0)
142
+ return { valid: false, error: 'no signers' };
143
+ return verifyClaimPolicy(claimPolicyOf(vtxo), witness, ctx);
144
+ }
@@ -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;
@@ -5,6 +5,7 @@ export const VTXO_ID_DOMAIN = 'totemsdk/omnia-vtxo/vtxo/v1';
5
5
  export const VTXO_POOL_ID_DOMAIN = 'totemsdk/omnia-vtxo/pool/v1';
6
6
  export const VTXO_LEAF_DOMAIN = 'totemsdk/omnia-vtxo/leaf/v1';
7
7
  export const VTXO_RECEIPT_DOMAIN = 'totemsdk/omnia-vtxo/receipt/v1';
8
+ export const VTXO_RECEIPT_BINDING_DOMAIN = 'totemsdk/omnia-vtxo/receipt-binding/v1';
8
9
  export const VTXO_BATCH_DOMAIN = 'totemsdk/omnia-vtxo/batch/v1';
9
10
  /**
10
11
  * Canonical batch commitment (RFC-038 §3.2): the operator signs this over the
@@ -13,6 +14,23 @@ export const VTXO_BATCH_DOMAIN = 'totemsdk/omnia-vtxo/batch/v1';
13
14
  export function computeBatchCommitment(poolId, epoch, root, orderedIds) {
14
15
  return sha3hex(`${VTXO_BATCH_DOMAIN}|${poolId}|${epoch}|${root}|${[...orderedIds].join(',')}`);
15
16
  }
17
+ /** Receipt commitment binds every semantically relevant receipt field. */
18
+ export function computeVtxoReceiptCommitment(receipt, root) {
19
+ return sha3hex([
20
+ VTXO_RECEIPT_BINDING_DOMAIN,
21
+ receipt.poolId,
22
+ receipt.epoch.toString(),
23
+ root,
24
+ receipt.receiptId,
25
+ receipt.op,
26
+ [...receipt.inputIds].join(','),
27
+ [...receipt.outputIds].join(','),
28
+ receipt.at.toString(),
29
+ ].join('|'));
30
+ }
31
+ export function computeVtxoReceiptCommitmentBytes(receipt, root) {
32
+ return hexToBytes(computeVtxoReceiptCommitment(receipt, root));
33
+ }
16
34
  /** The 32-byte digest of a batch commitment, for signing. */
17
35
  export function computeBatchCommitmentBytes(poolId, epoch, root, orderedIds) {
18
36
  return hexToBytes(computeBatchCommitment(poolId, epoch, root, orderedIds));
@@ -11,6 +11,10 @@ import { createRevisionedSnapshotStore, } from '@totemsdk/storage/snapshot';
11
11
  import { StorageError } from '@totemsdk/storage/errors';
12
12
  import { VtxoStatusError } from './errors.js';
13
13
  const DEFAULT_NAMESPACE = 'totem_omnia_vtxo:v1:';
14
+ const CONSUMED_STATUSES = new Set(['spent', 'transferred', 'split', 'merged', 'refreshed', 'exited']);
15
+ function isConsumed(status) {
16
+ return CONSUMED_STATUSES.has(status);
17
+ }
14
18
  function assertRegistryState(state) {
15
19
  if (typeof state !== 'object' || state === null || Array.isArray(state)) {
16
20
  throw new StorageError('omnia-vtxo registry state is not an object', 'corrupt');
@@ -44,6 +48,10 @@ export function createDurableOmniaVtxoStore(adapter, options = {}) {
44
48
  },
45
49
  async saveVtxo(vtxo) {
46
50
  await mutate((state) => {
51
+ const existing = state.vtxos[vtxo.vtxoId];
52
+ if (existing && isConsumed(existing.status) && !isConsumed(vtxo.status)) {
53
+ throw new VtxoStatusError(`Cannot resurrect VTXO ${vtxo.vtxoId}: status is '${existing.status}'`, existing.status);
54
+ }
47
55
  const vtxos = { ...state.vtxos, [vtxo.vtxoId]: vtxo };
48
56
  return { ...state, vtxos };
49
57
  });
@@ -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,66 @@
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 { deserializeTxDraft } from '@totemsdk/omnia';
9
+ import { exitTransactionDigest } from './exit-tx.js';
10
+ export function createVtxoExitEntitlement(params, result) {
11
+ return {
12
+ version: 1,
13
+ vtxoId: params.vtxo.vtxoId,
14
+ poolId: params.vtxo.poolId,
15
+ owner: params.vtxo.owner,
16
+ amount: params.vtxo.amount.toString(),
17
+ tokenId: params.vtxo.tokenId,
18
+ fundingCoinId: params.fundingCoinId,
19
+ fundingAddress: params.fundingAddress,
20
+ fundingScript: params.fundingScript,
21
+ holders: [...params.holders],
22
+ shares: params.shares.map((s) => s.toString()),
23
+ holderIndex: params.holderIndex,
24
+ root: params.root,
25
+ epoch: params.epoch,
26
+ exitBlock: params.exitBlock,
27
+ txHex: result.txHex,
28
+ digest: result.digest,
29
+ witnessHex: result.witnessHex,
30
+ };
31
+ }
32
+ export function verifyVtxoExitEntitlement(entitlement, vtxo) {
33
+ const errors = [];
34
+ if (entitlement.version !== 1)
35
+ errors.push('unsupported entitlement version');
36
+ if (entitlement.vtxoId !== vtxo.vtxoId)
37
+ errors.push('vtxo id mismatch');
38
+ if (entitlement.poolId !== vtxo.poolId)
39
+ errors.push('pool id mismatch');
40
+ if (entitlement.owner !== vtxo.owner)
41
+ errors.push('owner mismatch');
42
+ if (entitlement.amount !== vtxo.amount.toString())
43
+ errors.push('amount mismatch');
44
+ if (entitlement.tokenId !== vtxo.tokenId)
45
+ errors.push('token mismatch');
46
+ if (entitlement.holders[entitlement.holderIndex] !== vtxo.owner)
47
+ errors.push('holder index/owner mismatch');
48
+ if (entitlement.shares[entitlement.holderIndex] !== vtxo.amount.toString())
49
+ errors.push('holder share mismatch');
50
+ if (!entitlement.txHex)
51
+ errors.push('missing exit transaction');
52
+ if (!entitlement.witnessHex)
53
+ errors.push('missing exit witness');
54
+ if (entitlement.txHex && entitlement.digest) {
55
+ try {
56
+ const draft = deserializeTxDraft(entitlement.txHex);
57
+ const digest = Buffer.from(exitTransactionDigest(draft)).toString('hex');
58
+ if (digest !== entitlement.digest.replace(/^0x/i, ''))
59
+ errors.push('exit digest mismatch');
60
+ }
61
+ catch (err) {
62
+ errors.push(`exit transaction does not decode: ${err instanceof Error ? err.message : String(err)}`);
63
+ }
64
+ }
65
+ return { valid: errors.length === 0, errors };
66
+ }
package/dist/exit-tx.d.ts CHANGED
@@ -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
  }
package/dist/exit-tx.js CHANGED
@@ -11,7 +11,7 @@
11
11
  * The caller must pass the holder signer; neither the operator nor another holder
12
12
  * can build a valid witness for this holder's share.
13
13
  */
14
- import { computeOmniaTxDigest, omniaDraftToMinimaBytes, serializeTxDraft, } from '@totemsdk/omnia';
14
+ import { computeOmniaTxDigest, serializeOmniaWitness, omniaDraftToMinimaBytes, serializeTxDraft, } from '@totemsdk/omnia';
15
15
  import { serializeTxBody, mineTxPoW, TX_POW_MIN_DIFFICULTY } from '@totemsdk/txpow';
16
16
  import { concatBytes, wotsAddressData } from '@totemsdk/core';
17
17
  import { poolStateVariables } from './anchor.js';
@@ -32,14 +32,17 @@ export function buildExitTransactionDraft(params) {
32
32
  stateVariables: [],
33
33
  }];
34
34
  const carriesState = params.amount > share;
35
+ const remaining = params.shares.map((s, i) => (i === params.holderIndex ? 0n : s));
36
+ const exitState = carriesState
37
+ ? poolStateVariables(params.root, params.epoch, params.exitBlock, remaining)
38
+ : poolStateVariables(params.root, params.epoch, params.exitBlock, params.shares.map(() => 0n));
35
39
  if (carriesState) {
36
- const remaining = params.shares.map((s, i) => (i === params.holderIndex ? 0n : s));
37
40
  outputs.push({
38
41
  address: params.fundingAddress,
39
42
  amount: params.amount - share,
40
43
  tokenId: params.vtxo.tokenId,
41
44
  storeState: true,
42
- stateVariables: poolStateVariables(params.root, params.epoch, params.exitBlock, remaining),
45
+ stateVariables: exitState,
43
46
  });
44
47
  }
45
48
  return {
@@ -50,10 +53,11 @@ export function buildExitTransactionDraft(params) {
50
53
  amount: params.amount,
51
54
  tokenId: params.vtxo.tokenId,
52
55
  scriptHex: params.fundingScript,
56
+ ...(params.fundingMmrEntry !== undefined ? { mmrEntry: params.fundingMmrEntry } : {}),
53
57
  }],
54
58
  outputs,
55
59
  storeState: carriesState,
56
- stateVariables: [],
60
+ stateVariables: exitState,
57
61
  };
58
62
  }
59
63
  export function exitTransactionDigest(draft) {
@@ -67,6 +71,11 @@ export async function buildExitTransaction(params, deps) {
67
71
  const draft = buildExitTransactionDraft(params);
68
72
  const digest = exitTransactionDigest(draft);
69
73
  const signature = await deps.signDigest(digest);
74
+ const witness = serializeOmniaWitness({
75
+ signatures: [signature],
76
+ coinProofs: deps.coinProofs ?? [],
77
+ scriptProofs: deps.scriptProofs ?? [],
78
+ });
70
79
  const result = {
71
80
  vtxoId: params.vtxo.vtxoId,
72
81
  owner: params.vtxo.owner,
@@ -74,16 +83,28 @@ export async function buildExitTransaction(params, deps) {
74
83
  draftType: 'vtxo-exit',
75
84
  txHex: exitTransactionHex(draft),
76
85
  digest: Buffer.from(digest).toString('hex'),
77
- witnessHex: Buffer.from(signature).toString('hex'),
86
+ witnessHex: Buffer.from(witness).toString('hex'),
78
87
  };
79
88
  if (deps.chainProvider) {
80
- const mine = deps.mine ?? ((txBody, difficulty) => mineTxPoW(txBody, difficulty));
81
89
  const draftBytes = omniaDraftToMinimaBytes(draft);
82
- const txBody = serializeTxBody(draftBytes, signature);
83
- const mined = await mine(txBody, TX_POW_MIN_DIFFICULTY);
84
- const fullTxPoW = concatBytes(concatBytes(mined.minedHeaderBytes, new Uint8Array([0x01])), txBody);
85
- await deps.chainProvider.broadcastTxPoW(Buffer.from(fullTxPoW).toString('hex'));
86
- result.txpowId = Buffer.from(mined.txpowId).toString('hex');
90
+ if (typeof deps.chainProvider.submitTransaction === 'function') {
91
+ const submitted = await deps.chainProvider.submitTransaction(draftBytes, witness);
92
+ if (submitted?.success === false) {
93
+ throw new Error(`exitSubmit failed: ${submitted.message ?? 'unknown error'}`);
94
+ }
95
+ result.txpowId = submitted?.txpowid;
96
+ }
97
+ else {
98
+ const mine = deps.mine ?? ((txBody, difficulty) => mineTxPoW(txBody, difficulty));
99
+ const txBody = serializeTxBody(draftBytes, witness);
100
+ const mined = await mine(txBody, TX_POW_MIN_DIFFICULTY);
101
+ const fullTxPoW = concatBytes(concatBytes(mined.minedHeaderBytes, new Uint8Array([0x01])), txBody);
102
+ const broadcast = await deps.chainProvider.broadcastTxPoW(Buffer.from(fullTxPoW).toString('hex'));
103
+ if (broadcast?.success === false) {
104
+ throw new Error(`exitBroadcast failed: ${broadcast.message ?? 'unknown error'}`);
105
+ }
106
+ result.txpowId = broadcast?.txpowid ?? Buffer.from(mined.txpowId).toString('hex');
107
+ }
87
108
  }
88
109
  return result;
89
110
  }
package/dist/index.d.ts CHANGED
@@ -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';