@totemsdk/omnia-vtxo 0.2.0 → 0.3.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 +87 -0
- package/dist/anchor.js +107 -0
- package/dist/cjs/anchor.d.ts +87 -0
- package/dist/cjs/anchor.js +114 -0
- package/dist/cjs/commitment.d.ts +41 -0
- package/dist/cjs/commitment.js +173 -0
- package/dist/cjs/constants.d.ts +10 -0
- package/dist/cjs/constants.js +13 -0
- package/dist/cjs/durable-store.d.ts +33 -0
- package/dist/cjs/durable-store.js +93 -0
- package/dist/cjs/errors.d.ts +33 -0
- package/dist/cjs/errors.js +77 -0
- package/dist/cjs/exit-tx.d.ts +55 -0
- package/dist/cjs/exit-tx.js +81 -0
- package/dist/cjs/exit.d.ts +29 -0
- package/dist/cjs/exit.js +115 -0
- package/dist/cjs/index.d.ts +21 -0
- package/dist/cjs/index.js +37 -0
- package/dist/cjs/memory-store.d.ts +17 -0
- package/dist/cjs/memory-store.js +51 -0
- package/dist/cjs/merge.d.ts +8 -0
- package/dist/cjs/merge.js +106 -0
- package/dist/cjs/operator.d.ts +29 -0
- package/dist/cjs/operator.js +65 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/policy.d.ts +22 -0
- package/dist/cjs/policy.js +27 -0
- package/dist/cjs/pool-script.d.ts +19 -0
- package/dist/cjs/pool-script.js +41 -0
- package/dist/cjs/pool.d.ts +5 -0
- package/dist/cjs/pool.js +60 -0
- package/dist/cjs/receipt.d.ts +31 -0
- package/dist/cjs/receipt.js +52 -0
- package/dist/cjs/refresh.d.ts +8 -0
- package/dist/cjs/refresh.js +84 -0
- package/dist/cjs/serialization.d.ts +5 -0
- package/dist/cjs/serialization.js +31 -0
- package/dist/cjs/split.d.ts +8 -0
- package/dist/cjs/split.js +96 -0
- package/dist/cjs/transfer.d.ts +8 -0
- package/dist/cjs/transfer.js +152 -0
- package/dist/cjs/types.d.ts +166 -0
- package/dist/cjs/types.js +2 -0
- package/dist/cjs/verify.d.ts +23 -0
- package/dist/cjs/verify.js +121 -0
- package/dist/cjs/vtxo.d.ts +14 -0
- package/dist/cjs/vtxo.js +96 -0
- package/dist/commitment.d.ts +13 -0
- package/dist/commitment.js +39 -30
- package/dist/constants.d.ts +0 -2
- package/dist/constants.js +4 -9
- package/dist/durable-store.d.ts +33 -0
- package/dist/durable-store.js +90 -0
- package/dist/errors.js +10 -23
- package/dist/exit-tx.d.ts +55 -0
- package/dist/exit-tx.js +74 -0
- package/dist/exit.d.ts +12 -2
- package/dist/exit.js +39 -23
- package/dist/index.d.ts +6 -0
- package/dist/index.js +21 -31
- package/dist/memory-store.js +3 -7
- package/dist/merge.js +15 -19
- package/dist/operator.d.ts +29 -0
- package/dist/operator.js +61 -0
- package/dist/policy.d.ts +1 -1
- package/dist/policy.js +3 -7
- package/dist/pool-script.d.ts +19 -0
- package/dist/pool-script.js +36 -0
- package/dist/pool.js +21 -27
- package/dist/receipt.d.ts +31 -0
- package/dist/receipt.js +48 -0
- package/dist/refresh.js +10 -14
- package/dist/serialization.js +4 -10
- package/dist/split.js +13 -17
- package/dist/transfer.js +13 -17
- package/dist/types.d.ts +12 -1
- package/dist/types.js +1 -2
- package/dist/verify.js +7 -13
- package/dist/vtxo.js +14 -19
- package/package.json +19 -8
- package/rust/pkg/omnia_vtxo_wasm_bg.wasm +0 -0
- package/rust/pkg/package.json +1 -1
- package/rust/pkg-node/omnia_vtxo_wasm_bg.wasm +0 -0
- package/rust/pkg-node/package.json +1 -1
package/dist/exit.js
CHANGED
|
@@ -1,19 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
exports.markExiting = markExiting;
|
|
4
|
-
exports.markExited = markExited;
|
|
5
|
-
exports.createExitDraft = createExitDraft;
|
|
6
|
-
const errors_js_1 = require("./errors.js");
|
|
7
|
-
const constants_js_1 = require("./constants.js");
|
|
8
|
-
const commitment_js_1 = require("./commitment.js");
|
|
1
|
+
import { VtxoStatusError, VtxoExitError, VtxoProofError } from './errors.js';
|
|
2
|
+
import { computeReceiptId, computeVtxoLeaf } from './commitment.js';
|
|
9
3
|
/**
|
|
10
4
|
* Transitions an active VTXO to 'exiting' status.
|
|
11
5
|
* @param now - Timestamp in ms. Defaults to Date.now() if omitted — pass explicitly for determinism.
|
|
12
6
|
*/
|
|
13
|
-
function markExiting(vtxo, now) {
|
|
7
|
+
export function markExiting(vtxo, now) {
|
|
14
8
|
const ts = now !== undefined ? now : Date.now();
|
|
15
9
|
if (vtxo.status !== 'active') {
|
|
16
|
-
throw new
|
|
10
|
+
throw new VtxoStatusError(`Cannot initiate exit for VTXO ${vtxo.vtxoId}: status is '${vtxo.status}'`, vtxo.status);
|
|
17
11
|
}
|
|
18
12
|
return {
|
|
19
13
|
...vtxo,
|
|
@@ -29,18 +23,19 @@ function markExiting(vtxo, now) {
|
|
|
29
23
|
* Transitions an exiting VTXO to 'exited' status.
|
|
30
24
|
* @param now - Timestamp in ms. Defaults to Date.now() if omitted — pass explicitly for determinism.
|
|
31
25
|
*/
|
|
32
|
-
function markExited(vtxo,
|
|
33
|
-
const ts = now !== undefined ? now : Date.now();
|
|
26
|
+
export function markExited(vtxo, options = {}) {
|
|
27
|
+
const ts = options.now !== undefined ? options.now : Date.now();
|
|
34
28
|
if (vtxo.status !== 'exiting') {
|
|
35
|
-
throw new
|
|
29
|
+
throw new VtxoStatusError(`Cannot mark VTXO ${vtxo.vtxoId} as exited: status is '${vtxo.status}'`, vtxo.status);
|
|
36
30
|
}
|
|
37
31
|
return {
|
|
38
32
|
...vtxo,
|
|
39
33
|
status: 'exited',
|
|
40
34
|
updatedAt: ts,
|
|
35
|
+
...(options.txpowId ? { exitTxpowId: options.txpowId } : {}),
|
|
41
36
|
history: [
|
|
42
37
|
...vtxo.history,
|
|
43
|
-
{ op: 'exit_complete', at: ts },
|
|
38
|
+
{ op: 'exit_complete', at: ts, ...(options.txpowId ? { meta: { txpowId: options.txpowId } } : {}) },
|
|
44
39
|
],
|
|
45
40
|
};
|
|
46
41
|
}
|
|
@@ -51,20 +46,25 @@ function markExited(vtxo, now) {
|
|
|
51
46
|
function assertProofLeafValid(vtxo) {
|
|
52
47
|
if (!vtxo.proof.leaf)
|
|
53
48
|
return;
|
|
54
|
-
const expectedLeaf =
|
|
49
|
+
const expectedLeaf = computeVtxoLeaf(vtxo);
|
|
55
50
|
if (vtxo.proof.leaf !== expectedLeaf) {
|
|
56
|
-
throw new
|
|
51
|
+
throw new VtxoProofError(`Exit draft rejected: VTXO ${vtxo.vtxoId} proof leaf mismatch. ` +
|
|
57
52
|
`Expected ${expectedLeaf}, got ${vtxo.proof.leaf}`);
|
|
58
53
|
}
|
|
59
54
|
}
|
|
60
55
|
/**
|
|
61
|
-
* Creates a mock exit draft for a VTXO. Validates the proof leaf before
|
|
56
|
+
* Creates a mock exit draft for a VTXO. Validates the proof leaf before
|
|
57
|
+
* drafting, caps the exit at the verified share, and consumes the VTXO's
|
|
58
|
+
* receipt so a second exit draft is refused (double-exit prevention, #28).
|
|
62
59
|
* @param now - Timestamp in ms. Defaults to Date.now() if omitted — pass explicitly for determinism.
|
|
63
60
|
*/
|
|
64
|
-
function createExitDraft(vtxo, now) {
|
|
61
|
+
export function createExitDraft(vtxo, now) {
|
|
65
62
|
const ts = now !== undefined ? now : Date.now();
|
|
66
63
|
if (vtxo.status !== 'active' && vtxo.status !== 'exiting') {
|
|
67
|
-
throw new
|
|
64
|
+
throw new VtxoExitError(`Cannot create exit draft for VTXO ${vtxo.vtxoId}: status is '${vtxo.status}'`);
|
|
65
|
+
}
|
|
66
|
+
if (vtxo.exitConsumedAt !== undefined) {
|
|
67
|
+
throw new VtxoExitError(`Cannot create exit draft for VTXO ${vtxo.vtxoId}: exit already consumed by ${vtxo.exitReceiptId}`);
|
|
68
68
|
}
|
|
69
69
|
assertProofLeafValid(vtxo);
|
|
70
70
|
const draft = {
|
|
@@ -73,11 +73,12 @@ function createExitDraft(vtxo, now) {
|
|
|
73
73
|
owner: vtxo.owner,
|
|
74
74
|
amount: vtxo.amount,
|
|
75
75
|
tokenId: vtxo.tokenId,
|
|
76
|
-
draftType: '
|
|
76
|
+
draftType: 'vtxo-exit',
|
|
77
77
|
timelockSeconds: 86400,
|
|
78
78
|
createdAt: ts,
|
|
79
|
+
verifiedShare: vtxo.amount,
|
|
79
80
|
};
|
|
80
|
-
const receiptId =
|
|
81
|
+
const receiptId = computeReceiptId(vtxo.poolId, 'exit_initiated', [vtxo.vtxoId], [], ts);
|
|
81
82
|
const receipt = {
|
|
82
83
|
receiptId,
|
|
83
84
|
poolId: vtxo.poolId,
|
|
@@ -86,8 +87,23 @@ function createExitDraft(vtxo, now) {
|
|
|
86
87
|
outputIds: [],
|
|
87
88
|
epoch: vtxo.epoch,
|
|
88
89
|
at: ts,
|
|
89
|
-
signature:
|
|
90
|
-
draftType: '
|
|
90
|
+
signature: '',
|
|
91
|
+
draftType: 'vtxo-exit',
|
|
91
92
|
};
|
|
92
93
|
return { draft, receipt };
|
|
93
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Consume a VTXO's exit receipt — the single-spend marker that prevents a
|
|
97
|
+
* second exit draft for the same VTXO. Returns the updated VTXO.
|
|
98
|
+
*/
|
|
99
|
+
export function consumeExitReceipt(vtxo, receiptId, now) {
|
|
100
|
+
if (vtxo.exitConsumedAt !== undefined) {
|
|
101
|
+
throw new VtxoExitError(`VTXO ${vtxo.vtxoId} exit already consumed by ${vtxo.exitReceiptId}`);
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
...vtxo,
|
|
105
|
+
exitConsumedAt: now ?? Date.now(),
|
|
106
|
+
exitReceiptId: receiptId,
|
|
107
|
+
updatedAt: now ?? Date.now(),
|
|
108
|
+
};
|
|
109
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,11 @@ export * from './types.js';
|
|
|
2
2
|
export * from './errors.js';
|
|
3
3
|
export * from './constants.js';
|
|
4
4
|
export * from './commitment.js';
|
|
5
|
+
export * from './operator.js';
|
|
6
|
+
export * from './receipt.js';
|
|
7
|
+
export * from './pool-script.js';
|
|
8
|
+
export * from './anchor.js';
|
|
9
|
+
export * from './exit-tx.js';
|
|
5
10
|
export * from './pool.js';
|
|
6
11
|
export * from './vtxo.js';
|
|
7
12
|
export * from './transfer.js';
|
|
@@ -11,5 +16,6 @@ export * from './refresh.js';
|
|
|
11
16
|
export * from './exit.js';
|
|
12
17
|
export * from './verify.js';
|
|
13
18
|
export * from './memory-store.js';
|
|
19
|
+
export * from './durable-store.js';
|
|
14
20
|
export * from './serialization.js';
|
|
15
21
|
export * from './policy.js';
|
package/dist/index.js
CHANGED
|
@@ -1,31 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
__exportStar(require("./vtxo.js"), exports);
|
|
23
|
-
__exportStar(require("./transfer.js"), exports);
|
|
24
|
-
__exportStar(require("./split.js"), exports);
|
|
25
|
-
__exportStar(require("./merge.js"), exports);
|
|
26
|
-
__exportStar(require("./refresh.js"), exports);
|
|
27
|
-
__exportStar(require("./exit.js"), exports);
|
|
28
|
-
__exportStar(require("./verify.js"), exports);
|
|
29
|
-
__exportStar(require("./memory-store.js"), exports);
|
|
30
|
-
__exportStar(require("./serialization.js"), exports);
|
|
31
|
-
__exportStar(require("./policy.js"), exports);
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export * from './errors.js';
|
|
3
|
+
export * from './constants.js';
|
|
4
|
+
export * from './commitment.js';
|
|
5
|
+
export * from './operator.js';
|
|
6
|
+
export * from './receipt.js';
|
|
7
|
+
export * from './pool-script.js';
|
|
8
|
+
export * from './anchor.js';
|
|
9
|
+
export * from './exit-tx.js';
|
|
10
|
+
export * from './pool.js';
|
|
11
|
+
export * from './vtxo.js';
|
|
12
|
+
export * from './transfer.js';
|
|
13
|
+
export * from './split.js';
|
|
14
|
+
export * from './merge.js';
|
|
15
|
+
export * from './refresh.js';
|
|
16
|
+
export * from './exit.js';
|
|
17
|
+
export * from './verify.js';
|
|
18
|
+
export * from './memory-store.js';
|
|
19
|
+
export * from './durable-store.js';
|
|
20
|
+
export * from './serialization.js';
|
|
21
|
+
export * from './policy.js';
|
package/dist/memory-store.js
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
exports.MemoryOmniaVtxoStore = void 0;
|
|
4
|
-
const errors_js_1 = require("./errors.js");
|
|
5
|
-
class MemoryOmniaVtxoStore {
|
|
1
|
+
import { VtxoStatusError } from './errors.js';
|
|
2
|
+
export class MemoryOmniaVtxoStore {
|
|
6
3
|
constructor() {
|
|
7
4
|
this.pools = new Map();
|
|
8
5
|
this.vtxos = new Map();
|
|
@@ -36,7 +33,7 @@ class MemoryOmniaVtxoStore {
|
|
|
36
33
|
if (!vtxo)
|
|
37
34
|
throw new Error(`VTXO ${vtxoId} not found`);
|
|
38
35
|
if (vtxo.status !== 'active') {
|
|
39
|
-
throw new
|
|
36
|
+
throw new VtxoStatusError(`Cannot mark VTXO ${vtxoId} as spent: status is '${vtxo.status}'`, vtxo.status);
|
|
40
37
|
}
|
|
41
38
|
// NOTE: Date.now() fallback is intentional and documented above.
|
|
42
39
|
const ts = now !== undefined ? now : Date.now();
|
|
@@ -48,4 +45,3 @@ class MemoryOmniaVtxoStore {
|
|
|
48
45
|
});
|
|
49
46
|
}
|
|
50
47
|
}
|
|
51
|
-
exports.MemoryOmniaVtxoStore = MemoryOmniaVtxoStore;
|
package/dist/merge.js
CHANGED
|
@@ -1,26 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
exports.mergeVtxos = mergeVtxos;
|
|
4
|
-
const errors_js_1 = require("./errors.js");
|
|
5
|
-
const commitment_js_1 = require("./commitment.js");
|
|
6
|
-
const constants_js_1 = require("./constants.js");
|
|
1
|
+
import { VtxoStatusError, VtxoMergeError, VtxoOwnershipError } from './errors.js';
|
|
2
|
+
import { computeVtxoId, buildVtxoProofSet, computeReceiptId } from './commitment.js';
|
|
7
3
|
/**
|
|
8
4
|
* Merges multiple VTXOs into one.
|
|
9
5
|
* @param now - Timestamp in ms. Defaults to Date.now() if omitted — pass explicitly for determinism.
|
|
10
6
|
*/
|
|
11
|
-
function mergeVtxos(vtxos, params, now) {
|
|
7
|
+
export function mergeVtxos(vtxos, params, now) {
|
|
12
8
|
const ts = now !== undefined ? now : Date.now();
|
|
13
9
|
if (vtxos.length < 2) {
|
|
14
|
-
throw new
|
|
10
|
+
throw new VtxoMergeError('Merge requires at least 2 VTXOs');
|
|
15
11
|
}
|
|
16
12
|
const ids = vtxos.map(v => v.vtxoId);
|
|
17
13
|
const uniqueIds = new Set(ids);
|
|
18
14
|
if (uniqueIds.size !== ids.length) {
|
|
19
|
-
throw new
|
|
15
|
+
throw new VtxoMergeError('Duplicate VTXO IDs detected in merge — double-counting prevented');
|
|
20
16
|
}
|
|
21
17
|
for (const vtxo of vtxos) {
|
|
22
18
|
if (vtxo.status !== 'active') {
|
|
23
|
-
throw new
|
|
19
|
+
throw new VtxoStatusError(`Cannot merge VTXO ${vtxo.vtxoId}: status is '${vtxo.status}'`, vtxo.status);
|
|
24
20
|
}
|
|
25
21
|
}
|
|
26
22
|
const poolId = vtxos[0].poolId;
|
|
@@ -28,20 +24,20 @@ function mergeVtxos(vtxos, params, now) {
|
|
|
28
24
|
const inputOwner = vtxos[0].owner;
|
|
29
25
|
for (const vtxo of vtxos) {
|
|
30
26
|
if (vtxo.poolId !== poolId) {
|
|
31
|
-
throw new
|
|
27
|
+
throw new VtxoMergeError(`Cannot merge VTXOs from different pools: ${vtxo.poolId} vs ${poolId}`);
|
|
32
28
|
}
|
|
33
29
|
if (vtxo.tokenId !== tokenId) {
|
|
34
|
-
throw new
|
|
30
|
+
throw new VtxoMergeError(`Cannot merge VTXOs with different tokenIds: ${vtxo.tokenId} vs ${tokenId}`);
|
|
35
31
|
}
|
|
36
32
|
if (vtxo.owner !== inputOwner) {
|
|
37
|
-
throw new
|
|
33
|
+
throw new VtxoOwnershipError(`Cannot merge VTXOs with different owners: ${vtxo.owner} vs ${inputOwner}`);
|
|
38
34
|
}
|
|
39
35
|
}
|
|
40
36
|
if (params.owner !== inputOwner) {
|
|
41
|
-
throw new
|
|
37
|
+
throw new VtxoOwnershipError(`Merge output owner '${params.owner}' must match input owner '${inputOwner}'`);
|
|
42
38
|
}
|
|
43
39
|
const totalAmount = vtxos.reduce((sum, v) => sum + v.amount, BigInt(0));
|
|
44
|
-
const outputId =
|
|
40
|
+
const outputId = computeVtxoId({
|
|
45
41
|
poolId,
|
|
46
42
|
owner: params.owner,
|
|
47
43
|
amount: totalAmount,
|
|
@@ -75,7 +71,7 @@ function mergeVtxos(vtxos, params, now) {
|
|
|
75
71
|
siblings: [],
|
|
76
72
|
positions: [],
|
|
77
73
|
epoch: vtxos[0].epoch,
|
|
78
|
-
batchId:
|
|
74
|
+
batchId: '',
|
|
79
75
|
},
|
|
80
76
|
history: [
|
|
81
77
|
{
|
|
@@ -87,9 +83,9 @@ function mergeVtxos(vtxos, params, now) {
|
|
|
87
83
|
createdAt: ts,
|
|
88
84
|
updatedAt: ts,
|
|
89
85
|
};
|
|
90
|
-
const { proofs } =
|
|
86
|
+
const { proofs } = buildVtxoProofSet([rawOutput]);
|
|
91
87
|
const output = { ...rawOutput, proof: proofs[outputId] };
|
|
92
|
-
const receiptId =
|
|
88
|
+
const receiptId = computeReceiptId(poolId, 'merge', ids, [outputId], ts);
|
|
93
89
|
return {
|
|
94
90
|
inputs: inputVtxos,
|
|
95
91
|
output,
|
|
@@ -101,7 +97,7 @@ function mergeVtxos(vtxos, params, now) {
|
|
|
101
97
|
outputIds: [outputId],
|
|
102
98
|
epoch: vtxos[0].epoch,
|
|
103
99
|
at: ts,
|
|
104
|
-
signature:
|
|
100
|
+
signature: '',
|
|
105
101
|
},
|
|
106
102
|
};
|
|
107
103
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VTXO operator co-signature (RFC-038 V0).
|
|
3
|
+
*
|
|
4
|
+
* The operator signs the canonical batch commitment with a unified WOTS
|
|
5
|
+
* `TreeKey` (RFC-037), allocating one leaf per signature under a WOTS lease
|
|
6
|
+
* (RFC-013). This retires the mock operator-signature placeholder: a receipt's
|
|
7
|
+
* `signature` is now a real serialized `TreeSignature` over the commitment, and
|
|
8
|
+
* `verify` checks it against the operator's tree-root public key.
|
|
9
|
+
*
|
|
10
|
+
* The operator's identity is the TreeKey root public key, so a holder can verify
|
|
11
|
+
* a receipt without trusting transport identity.
|
|
12
|
+
*/
|
|
13
|
+
import { type TreeKey } from '@totemsdk/core';
|
|
14
|
+
import type { WotsLeaseProvider } from '@totemsdk/wots-lease';
|
|
15
|
+
import type { OmniaVtxoOperator } from './types.js';
|
|
16
|
+
/** Lease treeId for a pool's operator signatures. */
|
|
17
|
+
export declare function vtxoOperatorTreeId(poolId: string): string;
|
|
18
|
+
export interface VtxoOperatorOptions {
|
|
19
|
+
poolId: string;
|
|
20
|
+
/** The operator's unified WOTS TreeKey (RFC-037). */
|
|
21
|
+
treeKey: TreeKey;
|
|
22
|
+
/** Lease provider for one-time leaf allocation (RFC-013). */
|
|
23
|
+
leaseProvider: WotsLeaseProvider;
|
|
24
|
+
}
|
|
25
|
+
export interface VtxoOperatorWithIdentity extends OmniaVtxoOperator {
|
|
26
|
+
/** The operator's tree-root public key (its identity). */
|
|
27
|
+
readonly publicKey: string;
|
|
28
|
+
}
|
|
29
|
+
export declare function createVtxoOperator(options: VtxoOperatorOptions): VtxoOperatorWithIdentity;
|
package/dist/operator.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VTXO operator co-signature (RFC-038 V0).
|
|
3
|
+
*
|
|
4
|
+
* The operator signs the canonical batch commitment with a unified WOTS
|
|
5
|
+
* `TreeKey` (RFC-037), allocating one leaf per signature under a WOTS lease
|
|
6
|
+
* (RFC-013). This retires the mock operator-signature placeholder: a receipt's
|
|
7
|
+
* `signature` is now a real serialized `TreeSignature` over the commitment, and
|
|
8
|
+
* `verify` checks it against the operator's tree-root public key.
|
|
9
|
+
*
|
|
10
|
+
* The operator's identity is the TreeKey root public key, so a holder can verify
|
|
11
|
+
* a receipt without trusting transport identity.
|
|
12
|
+
*/
|
|
13
|
+
import { bytesToHex, deserializeTreeSignature, hexToBytes, serializeTreeSignature, sha3_256, verifyTreeSignature, } from '@totemsdk/core';
|
|
14
|
+
/** Keys per tree level — the flat leaf index is `l1 * KEYS_PER_LEVEL + l2`. */
|
|
15
|
+
const KEYS_PER_LEVEL = 64;
|
|
16
|
+
/** Lease treeId for a pool's operator signatures. */
|
|
17
|
+
export function vtxoOperatorTreeId(poolId) {
|
|
18
|
+
return `omnia-vtxo:${poolId}`;
|
|
19
|
+
}
|
|
20
|
+
export function createVtxoOperator(options) {
|
|
21
|
+
const { poolId, treeKey, leaseProvider } = options;
|
|
22
|
+
const rootPubkey = treeKey.getPublicKey();
|
|
23
|
+
// The TreeKey carries mutable state (setUses + sign); serialize signing so
|
|
24
|
+
// concurrent co-signs cannot race on a shared key.
|
|
25
|
+
let signLock = Promise.resolve();
|
|
26
|
+
const doSign = async (data) => {
|
|
27
|
+
const reservation = await leaseProvider.reserveKeyUse({
|
|
28
|
+
treeId: vtxoOperatorTreeId(poolId),
|
|
29
|
+
purpose: 'vtxo-operator',
|
|
30
|
+
payloadHash: bytesToHex(sha3_256(data)),
|
|
31
|
+
});
|
|
32
|
+
const flat = reservation.indices.l1 * KEYS_PER_LEVEL + reservation.indices.l2;
|
|
33
|
+
try {
|
|
34
|
+
treeKey.setUses(flat);
|
|
35
|
+
const signature = serializeTreeSignature(treeKey.sign(data));
|
|
36
|
+
await leaseProvider.commitKeyUse(reservation.reservationId, 'vtxo-operator');
|
|
37
|
+
return '0x' + bytesToHex(signature);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
await leaseProvider.burnReservation(reservation.reservationId, 'sign-failed').catch(() => { });
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
return {
|
|
45
|
+
poolId,
|
|
46
|
+
publicKey: '0x' + bytesToHex(rootPubkey).toLowerCase(),
|
|
47
|
+
sign(data) {
|
|
48
|
+
const run = signLock.then(() => doSign(data), () => doSign(data));
|
|
49
|
+
signLock = run.then(() => undefined, () => undefined);
|
|
50
|
+
return run;
|
|
51
|
+
},
|
|
52
|
+
async verify(data, sig) {
|
|
53
|
+
try {
|
|
54
|
+
return verifyTreeSignature(rootPubkey, data, deserializeTreeSignature(hexToBytes(sig)));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
package/dist/policy.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export interface VtxoExitIntent {
|
|
|
16
16
|
owner: string;
|
|
17
17
|
amount: bigint;
|
|
18
18
|
tokenId: string;
|
|
19
|
-
draftType: '
|
|
19
|
+
draftType: 'vtxo-exit';
|
|
20
20
|
}
|
|
21
21
|
export declare function buildVtxoTransferIntent(vtxo: OmniaVtxo, recipient: string, amount: bigint, nonce: string, changeNonce?: string): VtxoTransferIntent;
|
|
22
22
|
export declare function buildVtxoExitIntent(vtxo: OmniaVtxo): VtxoExitIntent;
|
package/dist/policy.js
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.buildVtxoTransferIntent = buildVtxoTransferIntent;
|
|
4
|
-
exports.buildVtxoExitIntent = buildVtxoExitIntent;
|
|
5
|
-
function buildVtxoTransferIntent(vtxo, recipient, amount, nonce, changeNonce) {
|
|
1
|
+
export function buildVtxoTransferIntent(vtxo, recipient, amount, nonce, changeNonce) {
|
|
6
2
|
return {
|
|
7
3
|
type: 'vtxo-transfer',
|
|
8
4
|
poolId: vtxo.poolId,
|
|
@@ -14,7 +10,7 @@ function buildVtxoTransferIntent(vtxo, recipient, amount, nonce, changeNonce) {
|
|
|
14
10
|
changeNonce,
|
|
15
11
|
};
|
|
16
12
|
}
|
|
17
|
-
function buildVtxoExitIntent(vtxo) {
|
|
13
|
+
export function buildVtxoExitIntent(vtxo) {
|
|
18
14
|
return {
|
|
19
15
|
type: 'vtxo-exit',
|
|
20
16
|
poolId: vtxo.poolId,
|
|
@@ -22,6 +18,6 @@ function buildVtxoExitIntent(vtxo) {
|
|
|
22
18
|
owner: vtxo.owner,
|
|
23
19
|
amount: vtxo.amount,
|
|
24
20
|
tokenId: vtxo.tokenId,
|
|
25
|
-
draftType: '
|
|
21
|
+
draftType: 'vtxo-exit',
|
|
26
22
|
};
|
|
27
23
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VTXO pool on-chain script (RFC-038 V1).
|
|
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").
|
|
8
|
+
*
|
|
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.
|
|
12
|
+
*/
|
|
13
|
+
/** Operator-controlled, epoch-monotone state-carrying pool script. */
|
|
14
|
+
export declare function buildPoolScript(operatorPublicKeyDigest: string): string;
|
|
15
|
+
export declare function poolScriptAddress(script: string): string;
|
|
16
|
+
export declare function buildAndHashPoolScript(operatorPublicKeyDigest: string): {
|
|
17
|
+
script: string;
|
|
18
|
+
address: string;
|
|
19
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VTXO pool on-chain script (RFC-038 V1).
|
|
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").
|
|
8
|
+
*
|
|
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.
|
|
12
|
+
*/
|
|
13
|
+
import { computeScriptAddress } from '@totemsdk/core';
|
|
14
|
+
function operatorPkHex(operatorPublicKeyDigest) {
|
|
15
|
+
const raw = operatorPublicKeyDigest.replace(/^0x/i, '');
|
|
16
|
+
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)}`);
|
|
18
|
+
}
|
|
19
|
+
return '0X' + raw.toUpperCase();
|
|
20
|
+
}
|
|
21
|
+
/** Operator-controlled, epoch-monotone state-carrying pool script. */
|
|
22
|
+
export function buildPoolScript(operatorPublicKeyDigest) {
|
|
23
|
+
return [
|
|
24
|
+
'LET ROOT=STATE(100)',
|
|
25
|
+
'LET EPOCH=STATE(101)',
|
|
26
|
+
'ASSERT EPOCH GT PREVSTATE(101)',
|
|
27
|
+
`RETURN SIGNEDBY(${operatorPkHex(operatorPublicKeyDigest)})`,
|
|
28
|
+
].join('\n');
|
|
29
|
+
}
|
|
30
|
+
export function poolScriptAddress(script) {
|
|
31
|
+
return computeScriptAddress(script);
|
|
32
|
+
}
|
|
33
|
+
export function buildAndHashPoolScript(operatorPublicKeyDigest) {
|
|
34
|
+
const script = buildPoolScript(operatorPublicKeyDigest);
|
|
35
|
+
return { script, address: poolScriptAddress(script) };
|
|
36
|
+
}
|
package/dist/pool.js
CHANGED
|
@@ -1,24 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
exports.updatePoolRoot = updatePoolRoot;
|
|
6
|
-
exports.advancePoolEpoch = advancePoolEpoch;
|
|
7
|
-
const errors_js_1 = require("./errors.js");
|
|
8
|
-
const constants_js_1 = require("./constants.js");
|
|
9
|
-
const commitment_js_1 = require("./commitment.js");
|
|
10
|
-
function createPool(params, now) {
|
|
1
|
+
import { VtxoPoolCapacityError, VtxoPolicyError } from './errors.js';
|
|
2
|
+
import { DEFAULT_POLICY, EMPTY_TREE_ROOT, EPOCH_ZERO } from './constants.js';
|
|
3
|
+
import { computePoolId } from './commitment.js';
|
|
4
|
+
export function createPool(params, now) {
|
|
11
5
|
const policy = {
|
|
12
|
-
minAmount: params.policy?.minAmount ??
|
|
13
|
-
maxAmount: params.policy?.maxAmount ??
|
|
14
|
-
maxMergeInputs: params.policy?.maxMergeInputs ??
|
|
15
|
-
maxSplitOutputs: params.policy?.maxSplitOutputs ??
|
|
16
|
-
exitTimelockSeconds: params.policy?.exitTimelockSeconds ??
|
|
6
|
+
minAmount: params.policy?.minAmount ?? DEFAULT_POLICY.minAmount,
|
|
7
|
+
maxAmount: params.policy?.maxAmount ?? DEFAULT_POLICY.maxAmount,
|
|
8
|
+
maxMergeInputs: params.policy?.maxMergeInputs ?? DEFAULT_POLICY.maxMergeInputs,
|
|
9
|
+
maxSplitOutputs: params.policy?.maxSplitOutputs ?? DEFAULT_POLICY.maxSplitOutputs,
|
|
10
|
+
exitTimelockSeconds: params.policy?.exitTimelockSeconds ?? DEFAULT_POLICY.exitTimelockSeconds,
|
|
17
11
|
};
|
|
18
12
|
if (params.totalCapacity <= BigInt(0)) {
|
|
19
|
-
throw new
|
|
13
|
+
throw new VtxoPolicyError('Pool totalCapacity must be > 0');
|
|
20
14
|
}
|
|
21
|
-
const poolId =
|
|
15
|
+
const poolId = computePoolId({
|
|
22
16
|
operator: params.operator,
|
|
23
17
|
tokenId: params.tokenId,
|
|
24
18
|
nonce: params.nonce,
|
|
@@ -29,32 +23,32 @@ function createPool(params, now) {
|
|
|
29
23
|
tokenId: params.tokenId,
|
|
30
24
|
totalCapacity: params.totalCapacity,
|
|
31
25
|
availableCapacity: params.totalCapacity,
|
|
32
|
-
epoch:
|
|
33
|
-
commitmentRoot:
|
|
26
|
+
epoch: EPOCH_ZERO,
|
|
27
|
+
commitmentRoot: EMPTY_TREE_ROOT,
|
|
34
28
|
createdAt: now,
|
|
35
29
|
policy,
|
|
36
30
|
};
|
|
37
31
|
}
|
|
38
|
-
function assertPoolCanMint(pool, amount) {
|
|
32
|
+
export function assertPoolCanMint(pool, amount) {
|
|
39
33
|
if (amount <= BigInt(0)) {
|
|
40
|
-
throw new
|
|
34
|
+
throw new VtxoPolicyError(`Mint amount must be > 0, got ${amount}`);
|
|
41
35
|
}
|
|
42
36
|
if (amount < pool.policy.minAmount) {
|
|
43
|
-
throw new
|
|
37
|
+
throw new VtxoPolicyError(`Mint amount ${amount} is below pool minimum ${pool.policy.minAmount}`);
|
|
44
38
|
}
|
|
45
39
|
if (amount > pool.policy.maxAmount) {
|
|
46
|
-
throw new
|
|
40
|
+
throw new VtxoPolicyError(`Mint amount ${amount} exceeds pool maximum ${pool.policy.maxAmount}`);
|
|
47
41
|
}
|
|
48
42
|
if (amount > pool.availableCapacity) {
|
|
49
|
-
throw new
|
|
43
|
+
throw new VtxoPoolCapacityError(`Insufficient pool capacity: requested ${amount}, available ${pool.availableCapacity}`, amount, pool.availableCapacity);
|
|
50
44
|
}
|
|
51
45
|
}
|
|
52
|
-
function updatePoolRoot(pool, root) {
|
|
46
|
+
export function updatePoolRoot(pool, root) {
|
|
53
47
|
return { ...pool, commitmentRoot: root };
|
|
54
48
|
}
|
|
55
|
-
function advancePoolEpoch(pool, newEpoch) {
|
|
49
|
+
export function advancePoolEpoch(pool, newEpoch) {
|
|
56
50
|
if (newEpoch <= pool.epoch) {
|
|
57
|
-
throw new
|
|
51
|
+
throw new VtxoPolicyError(`New epoch ${newEpoch} must be greater than current epoch ${pool.epoch}`);
|
|
58
52
|
}
|
|
59
53
|
return { ...pool, epoch: newEpoch };
|
|
60
54
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VTXO receipt co-signing (RFC-038 V0, verify-before-sign per §3.5).
|
|
3
|
+
*
|
|
4
|
+
* A mutation emits its `VtxoOperatorReceipt` **unsigned**; the operator co-signs
|
|
5
|
+
* it only after **recomputing** the batch root from the batch's leaves and
|
|
6
|
+
* checking epoch monotonicity, output membership, and (when supplied) amount
|
|
7
|
+
* conservation. This prevents a caller from getting the operator to co-sign an
|
|
8
|
+
* arbitrary `(root, epoch, ids)` tuple whose root does not commit the outputs.
|
|
9
|
+
*/
|
|
10
|
+
import type { OmniaVtxo, OmniaVtxoOperator, VtxoOperatorReceipt } from './types.js';
|
|
11
|
+
/** The batch the receipt belongs to — the operator's source of truth. */
|
|
12
|
+
export interface VtxoReceiptBatch {
|
|
13
|
+
/** The VTXOs committed in this epoch's batch. */
|
|
14
|
+
outputs: OmniaVtxo[];
|
|
15
|
+
/** The batch root the receipt claims (the operator recomputes and compares). */
|
|
16
|
+
root: string;
|
|
17
|
+
/** The batch epoch. */
|
|
18
|
+
epoch: number;
|
|
19
|
+
/** The pool's current epoch, for monotonicity (must be strictly less). */
|
|
20
|
+
previousEpoch?: number;
|
|
21
|
+
/** Optional: total input amount, for a conservation check. */
|
|
22
|
+
inputsAmount?: bigint;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Co-sign a receipt with the operator, after verifying the batch.
|
|
26
|
+
* @throws if the root does not commit the leaves, the epoch is stale, an output
|
|
27
|
+
* is not in the batch, or conservation fails.
|
|
28
|
+
*/
|
|
29
|
+
export declare function signVtxoReceipt(operator: OmniaVtxoOperator, receipt: VtxoOperatorReceipt, batch: VtxoReceiptBatch): Promise<VtxoOperatorReceipt>;
|
|
30
|
+
/** Verify a co-signed receipt against the operator and the batch root. */
|
|
31
|
+
export declare function verifyVtxoReceipt(operator: OmniaVtxoOperator, receipt: VtxoOperatorReceipt, root: string): Promise<boolean>;
|
package/dist/receipt.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VTXO receipt co-signing (RFC-038 V0, verify-before-sign per §3.5).
|
|
3
|
+
*
|
|
4
|
+
* A mutation emits its `VtxoOperatorReceipt` **unsigned**; the operator co-signs
|
|
5
|
+
* it only after **recomputing** the batch root from the batch's leaves and
|
|
6
|
+
* checking epoch monotonicity, output membership, and (when supplied) amount
|
|
7
|
+
* conservation. This prevents a caller from getting the operator to co-sign an
|
|
8
|
+
* arbitrary `(root, epoch, ids)` tuple whose root does not commit the outputs.
|
|
9
|
+
*/
|
|
10
|
+
import { computeBatchCommitmentBytes, computeCommitmentRoot, computeVtxoLeaf } from './commitment.js';
|
|
11
|
+
/**
|
|
12
|
+
* Co-sign a receipt with the operator, after verifying the batch.
|
|
13
|
+
* @throws if the root does not commit the leaves, the epoch is stale, an output
|
|
14
|
+
* is not in the batch, or conservation fails.
|
|
15
|
+
*/
|
|
16
|
+
export async function signVtxoReceipt(operator, receipt, batch) {
|
|
17
|
+
if (receipt.epoch !== batch.epoch) {
|
|
18
|
+
throw new Error(`receipt epoch ${receipt.epoch} does not match batch epoch ${batch.epoch}`);
|
|
19
|
+
}
|
|
20
|
+
if (batch.previousEpoch !== undefined && batch.epoch <= batch.previousEpoch) {
|
|
21
|
+
throw new Error(`batch epoch ${batch.epoch} is not greater than the pool epoch ${batch.previousEpoch}`);
|
|
22
|
+
}
|
|
23
|
+
const idSet = new Set(batch.outputs.map((v) => v.vtxoId));
|
|
24
|
+
for (const id of receipt.outputIds) {
|
|
25
|
+
if (!idSet.has(id))
|
|
26
|
+
throw new Error(`receipt output '${id}' is not a VTXO in the batch`);
|
|
27
|
+
}
|
|
28
|
+
const recomputedRoot = computeCommitmentRoot(batch.outputs.map(computeVtxoLeaf));
|
|
29
|
+
if (recomputedRoot !== batch.root) {
|
|
30
|
+
throw new Error('batch root does not commit the batch leaves');
|
|
31
|
+
}
|
|
32
|
+
if (batch.inputsAmount !== undefined) {
|
|
33
|
+
const outputSum = batch.outputs.reduce((sum, v) => sum + v.amount, 0n);
|
|
34
|
+
if (outputSum > batch.inputsAmount) {
|
|
35
|
+
throw new Error(`conservation violated: outputs ${outputSum} exceed inputs ${batch.inputsAmount}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const data = computeBatchCommitmentBytes(receipt.poolId, receipt.epoch, batch.root, receipt.outputIds);
|
|
39
|
+
const signature = await operator.sign(data);
|
|
40
|
+
return { ...receipt, signature };
|
|
41
|
+
}
|
|
42
|
+
/** Verify a co-signed receipt against the operator and the batch root. */
|
|
43
|
+
export async function verifyVtxoReceipt(operator, receipt, root) {
|
|
44
|
+
if (!receipt.signature)
|
|
45
|
+
return false;
|
|
46
|
+
const data = computeBatchCommitmentBytes(receipt.poolId, receipt.epoch, root, receipt.outputIds);
|
|
47
|
+
return operator.verify(data, receipt.signature);
|
|
48
|
+
}
|