@odatano/contract-m402-vault 0.0.0-stage → 0.2.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/LICENSE +178 -0
- package/NOTICE +9 -0
- package/README.md +42 -2
- package/contract.json +25 -0
- package/managed/m402-vault/compiler/contract-info.json +346 -0
- package/managed/m402-vault/compiler/contract-manifest.json +139 -0
- package/managed/m402-vault/contract/index.d.ts +125 -0
- package/managed/m402-vault/contract/index.js +2216 -0
- package/managed/m402-vault/contract/index.js.map +8 -0
- package/managed/m402-vault/keys/deposit.verifier +0 -0
- package/managed/m402-vault/keys/manifest.json +69 -0
- package/managed/m402-vault/keys/pay.verifier +0 -0
- package/managed/m402-vault/keys/redeem.verifier +0 -0
- package/managed/m402-vault/keys/registerService.verifier +0 -0
- package/managed/m402-vault/keys/withdraw.verifier +0 -0
- package/managed/m402-vault/zkir/deposit.bzkir +0 -0
- package/managed/m402-vault/zkir/deposit.zkir +126 -0
- package/managed/m402-vault/zkir/pay.bzkir +0 -0
- package/managed/m402-vault/zkir/pay.zkir +107 -0
- package/managed/m402-vault/zkir/redeem.bzkir +0 -0
- package/managed/m402-vault/zkir/redeem.zkir +87 -0
- package/managed/m402-vault/zkir/registerService.bzkir +0 -0
- package/managed/m402-vault/zkir/registerService.zkir +56 -0
- package/managed/m402-vault/zkir/withdraw.bzkir +0 -0
- package/managed/m402-vault/zkir/withdraw.zkir +86 -0
- package/package.json +48 -5
- package/src/m402-vault.compact +137 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": { "major": 3, "minor": 1 },
|
|
3
|
+
"do_communications_commitment": true,
|
|
4
|
+
"inputs": [
|
|
5
|
+
{ "name": "%serviceId.0", "type": "Scalar<BLS12-381>" },
|
|
6
|
+
{ "name": "%serviceId.1", "type": "Scalar<BLS12-381>" },
|
|
7
|
+
{ "name": "%amount.2", "type": "Scalar<BLS12-381>" }
|
|
8
|
+
],
|
|
9
|
+
"outputs": [
|
|
10
|
+
],
|
|
11
|
+
"instructions": [
|
|
12
|
+
{ "op": "constrain_bits", "val": "%serviceId.0", "bits": 8 },
|
|
13
|
+
{ "op": "constrain_bits", "val": "%serviceId.1", "bits": 248 },
|
|
14
|
+
{ "op": "constrain_bits", "val": "%amount.2", "bits": 64 },
|
|
15
|
+
{ "op": "less_than", "output": "%t.3", "a": "0x00", "b": "%amount.2", "bits": 64 },
|
|
16
|
+
{ "op": "assert", "cond": "%t.3" },
|
|
17
|
+
{ "op": "public_input", "type": "Scalar<BLS12-381>", "output": "%t.4", "guard": null },
|
|
18
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x30"] },
|
|
19
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x50", "0x01", "0x01", "0x01"] },
|
|
20
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x10", "0x01", "0x01", "0x20", "%serviceId.0", "%serviceId.1"] },
|
|
21
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x18"] },
|
|
22
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x0d", "0x01", "0x01", "%t.4"] },
|
|
23
|
+
{ "op": "assert", "cond": "%t.4" },
|
|
24
|
+
{ "op": "public_input", "type": "Scalar<BLS12-381>", "output": "%owner.5", "guard": null },
|
|
25
|
+
{ "op": "public_input", "type": "Scalar<BLS12-381>", "output": "%owner.6", "guard": null },
|
|
26
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x30"] },
|
|
27
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x50", "0x01", "0x01", "0x01"] },
|
|
28
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x50", "0x01", "0x20", "%serviceId.0", "%serviceId.1"] },
|
|
29
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x0c", "0x01", "0x20", "%owner.5", "%owner.6"] },
|
|
30
|
+
{ "op": "public_input", "type": "Scalar<BLS12-381>", "output": "%t.7", "guard": null },
|
|
31
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x30"] },
|
|
32
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x50", "0x01", "0x01", "0x03"] },
|
|
33
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x10", "0x01", "0x01", "0x20", "%owner.5", "%owner.6"] },
|
|
34
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x18"] },
|
|
35
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x0d", "0x01", "0x01", "%t.7"] },
|
|
36
|
+
{ "op": "assert", "cond": "%t.7" },
|
|
37
|
+
{ "op": "public_input", "type": "Scalar<BLS12-381>", "output": "%balance.8", "guard": null },
|
|
38
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x30"] },
|
|
39
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x50", "0x01", "0x01", "0x03"] },
|
|
40
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x50", "0x01", "0x20", "%owner.5", "%owner.6"] },
|
|
41
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x0c", "0x01", "0x08", "%balance.8"] },
|
|
42
|
+
{ "op": "less_than", "output": "%t.9", "a": "%balance.8", "b": "%amount.2", "bits": 64 },
|
|
43
|
+
{ "op": "cond_select", "output": "%t.10", "bit": "%t.9", "a": "0x00", "b": "0x01" },
|
|
44
|
+
{ "op": "assert", "cond": "%t.10" },
|
|
45
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x40"] },
|
|
46
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x80", "0x01", "0x01", "0x07"] },
|
|
47
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x10", "0x01", "0x03", "0x01", "0x20", "0x20", "0x01", "0x00", "0x00", "0x00", "0x00"] },
|
|
48
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x31"] },
|
|
49
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x31"] },
|
|
50
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x18"] },
|
|
51
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x10", "0x01", "0x01", "0x10", "%amount.2"] },
|
|
52
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x40"] },
|
|
53
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x08"] },
|
|
54
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x12", "0x04"] },
|
|
55
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x32"] },
|
|
56
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x32"] },
|
|
57
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x60", "-0x01"] },
|
|
58
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x14"] },
|
|
59
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0xa2"] },
|
|
60
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x40"] },
|
|
61
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x40"] },
|
|
62
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x80", "0x01", "0x01", "0x08"] },
|
|
63
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x10", "0x01", "0x06", "0x01", "0x20", "0x20", "0x01", "0x20", "0x20", "0x01", "0x00", "0x00", "0x00", "0x00", "0x00", "0x00", "0x00", "%owner.5", "%owner.6"] },
|
|
64
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x31"] },
|
|
65
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x31"] },
|
|
66
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x18"] },
|
|
67
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x10", "0x01", "0x01", "0x10", "%amount.2"] },
|
|
68
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x40"] },
|
|
69
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x08"] },
|
|
70
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x12", "0x04"] },
|
|
71
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x32"] },
|
|
72
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x32"] },
|
|
73
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x60", "-0x01"] },
|
|
74
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x14"] },
|
|
75
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0xa2"] },
|
|
76
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x40"] },
|
|
77
|
+
{ "op": "assert", "cond": "%t.10" },
|
|
78
|
+
{ "op": "neg", "output": "%neg.11", "a": "%amount.2" },
|
|
79
|
+
{ "op": "add", "output": "%tmp.12", "a": "%balance.8", "b": "%neg.11" },
|
|
80
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x70", "0x01", "0x01", "0x03"] },
|
|
81
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x10", "0x01", "0x01", "0x20", "%owner.5", "%owner.6"] },
|
|
82
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x11", "0x01", "0x01", "0x08", "%tmp.12"] },
|
|
83
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0x91"] },
|
|
84
|
+
{ "op": "impact", "guard": "0x01", "inputs": ["0xa1"] }
|
|
85
|
+
]
|
|
86
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,49 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@odatano/contract-m402-vault",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "m402 payment vault Compact contract: NIGHT pooled against a shielded credit, services paid in private, receipts checked in the public ledger.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"author": "ODATANO",
|
|
7
|
+
"homepage": "https://github.com/ODATANO/NIGHTGATE-CONTRACTS#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/ODATANO/NIGHTGATE-CONTRACTS.git",
|
|
11
|
+
"directory": "packages/m402-vault"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"midnight",
|
|
15
|
+
"compact",
|
|
16
|
+
"nightgate",
|
|
17
|
+
"payment",
|
|
18
|
+
"m402"
|
|
19
|
+
],
|
|
20
|
+
"type": "module",
|
|
21
|
+
"main": "./managed/m402-vault/contract/index.js",
|
|
22
|
+
"types": "./managed/m402-vault/contract/index.d.ts",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./managed/m402-vault/contract/index.d.ts",
|
|
26
|
+
"default": "./managed/m402-vault/contract/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./contract.json": "./contract.json",
|
|
29
|
+
"./package.json": "./package.json"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"contract.json",
|
|
33
|
+
"src/*.compact",
|
|
34
|
+
"managed/**/*",
|
|
35
|
+
"!managed/**/*.prover",
|
|
36
|
+
"README.md",
|
|
37
|
+
"LICENSE",
|
|
38
|
+
"NOTICE"
|
|
39
|
+
],
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"@midnight-ntwrk/compact-runtime": "0.21.0"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"prepublishOnly": "npm --prefix ../.. run check"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// m402 payment vault: NIGHT pooled in the contract, a shielded credit minted 1:1.
|
|
2
|
+
// Coins and secrets are circuit parameters, so a caller needs no witness of its own.
|
|
3
|
+
|
|
4
|
+
// Modified from m402 (https://github.com/julianariel/m402), Apache-2.0.
|
|
5
|
+
// Copyright 2026 The m402 contributors. Changed: witnesses became circuit parameters.
|
|
6
|
+
pragma language_version >= 0.23;
|
|
7
|
+
|
|
8
|
+
import CompactStandardLibrary;
|
|
9
|
+
|
|
10
|
+
export ledger servicePrice: Map<Bytes<32>, Uint<64>>;
|
|
11
|
+
export ledger serviceOwner: Map<Bytes<32>, Bytes<32>>;
|
|
12
|
+
export ledger receipts: Set<Bytes<32>>;
|
|
13
|
+
export ledger merchantBalance: Map<Bytes<32>, Uint<64>>;
|
|
14
|
+
export ledger mintCounter: Counter;
|
|
15
|
+
|
|
16
|
+
pure circuit creditDomain(): Bytes<32> { return pad(32, "m402:credit:v1"); }
|
|
17
|
+
|
|
18
|
+
// The colour of this vault's credit, derivable off-chain with no proof; `self` is the
|
|
19
|
+
// vault's address (a pure circuit has no ledger access).
|
|
20
|
+
export pure circuit creditColor(self: ContractAddress): Bytes<32> {
|
|
21
|
+
return tokenType(creditDomain(), self);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// The redemption credential and the selective-disclosure opening: the payer keeps
|
|
25
|
+
// the secret, the chain sees only this hash.
|
|
26
|
+
export pure circuit deriveReceipt(secret: Bytes<32>, serviceId: Bytes<32>): Bytes<32> {
|
|
27
|
+
return persistentHash<Vector<3, Bytes<32>>>(
|
|
28
|
+
[pad(32, "m402:receipt:v1"), secret, serviceId]
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// The service id binds owner, salt and price: a front-runner can copy none of them
|
|
33
|
+
// without landing on another id.
|
|
34
|
+
export pure circuit deriveServiceId(
|
|
35
|
+
owner: Bytes<32>,
|
|
36
|
+
salt: Bytes<32>,
|
|
37
|
+
price: Uint<64>
|
|
38
|
+
): Bytes<32> {
|
|
39
|
+
return persistentHash<Vector<4, Bytes<32>>>(
|
|
40
|
+
[pad(32, "m402:sid:v1"), owner, salt, price as Bytes<32>]
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export circuit registerService(salt: Bytes<32>, price: Uint<64>, owner: Bytes<32>): [] {
|
|
45
|
+
assert(price > 0, "price must be positive");
|
|
46
|
+
assert(owner != pad(32, ""), "owner must be set");
|
|
47
|
+
|
|
48
|
+
const serviceId = deriveServiceId(disclose(owner), disclose(salt), disclose(price));
|
|
49
|
+
// first come, immutable: neither map may be overwritten
|
|
50
|
+
assert(!servicePrice.member(serviceId), "already registered");
|
|
51
|
+
assert(!serviceOwner.member(serviceId), "already registered");
|
|
52
|
+
|
|
53
|
+
servicePrice.insert(serviceId, disclose(price));
|
|
54
|
+
serviceOwner.insert(serviceId, disclose(owner));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Unshielded NIGHT in, shielded credit out to the caller's own key. `nonceSeed` is
|
|
58
|
+
// fresh random bytes from the caller: it hides which deposit funded which payment.
|
|
59
|
+
export circuit deposit(amount: Uint<64>, nonceSeed: Bytes<32>): [] {
|
|
60
|
+
assert(amount > 0, "amount must be positive");
|
|
61
|
+
receiveUnshielded(nativeToken(), disclose(amount) as Uint<128>);
|
|
62
|
+
|
|
63
|
+
const nonce = disclose(evolveNonce(mintCounter as Field as Uint<64>, nonceSeed));
|
|
64
|
+
const coin = mintShieldedToken(
|
|
65
|
+
creditDomain(),
|
|
66
|
+
disclose(amount),
|
|
67
|
+
nonce,
|
|
68
|
+
right<ZswapCoinPublicKey, ContractAddress>(kernel.self())
|
|
69
|
+
);
|
|
70
|
+
// the caller's key as the destination is sound: a lie misdirects only its own deposit
|
|
71
|
+
sendImmediateShielded(
|
|
72
|
+
coin,
|
|
73
|
+
left<ZswapCoinPublicKey, ContractAddress>(ownPublicKey()),
|
|
74
|
+
coin.value
|
|
75
|
+
);
|
|
76
|
+
mintCounter.increment(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Pays a service with one credit coin of exactly its price; `receiptSecret` is the
|
|
80
|
+
// payer's fresh random credential, only its hash goes on chain.
|
|
81
|
+
export circuit pay(serviceId: Bytes<32>, credit: ShieldedCoinInfo, receiptSecret: Bytes<32>): [] {
|
|
82
|
+
const sid = disclose(serviceId);
|
|
83
|
+
assert(servicePrice.member(sid), "unknown service");
|
|
84
|
+
const price = servicePrice.lookup(sid);
|
|
85
|
+
|
|
86
|
+
// receiveShielded does not constrain the colour: a self-minted token must not pay
|
|
87
|
+
assert(credit.color == tokenType(creditDomain(), kernel.self()), "not an m402 credit");
|
|
88
|
+
// the whole coin is consumed and only the price credited: an overpaying coin burns the rest
|
|
89
|
+
assert(credit.value == price as Uint<128>, "wrong amount");
|
|
90
|
+
|
|
91
|
+
const receipt = deriveReceipt(receiptSecret, sid);
|
|
92
|
+
assert(!receipts.member(disclose(receipt)), "receipt reused");
|
|
93
|
+
|
|
94
|
+
receiveShielded(disclose(credit));
|
|
95
|
+
receipts.insert(disclose(receipt));
|
|
96
|
+
|
|
97
|
+
const owner = serviceOwner.lookup(sid);
|
|
98
|
+
const prior = merchantBalance.member(owner) ? merchantBalance.lookup(owner) : 0 as Uint<64>;
|
|
99
|
+
merchantBalance.insert(owner, (prior + price) as Uint<64>);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Cashes a credit coin back to NIGHT. Holding the coin is the authorisation (Zswap
|
|
103
|
+
// validates the spend), so the caller names any recipient for its own funds.
|
|
104
|
+
export circuit redeem(recipient: Bytes<32>, credit: ShieldedCoinInfo): [] {
|
|
105
|
+
assert(recipient != pad(32, ""), "recipient must be set");
|
|
106
|
+
|
|
107
|
+
assert(credit.color == tokenType(creditDomain(), kernel.self()), "not an m402 credit");
|
|
108
|
+
assert(credit.value > 0, "nothing to redeem");
|
|
109
|
+
|
|
110
|
+
receiveShielded(disclose(credit));
|
|
111
|
+
sendUnshielded(
|
|
112
|
+
nativeToken(),
|
|
113
|
+
disclose(credit.value),
|
|
114
|
+
right<ContractAddress, UserAddress>(UserAddress { bytes: disclose(recipient) })
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export circuit withdraw(serviceId: Bytes<32>, amount: Uint<64>): [] {
|
|
119
|
+
const sid = disclose(serviceId);
|
|
120
|
+
// a zero amount would be a free way to contend on merchantBalance
|
|
121
|
+
assert(amount > 0, "amount must be positive");
|
|
122
|
+
assert(serviceOwner.member(sid), "unknown service");
|
|
123
|
+
const owner = serviceOwner.lookup(sid);
|
|
124
|
+
|
|
125
|
+
assert(merchantBalance.member(owner), "no balance");
|
|
126
|
+
const balance = merchantBalance.lookup(owner);
|
|
127
|
+
assert(balance >= disclose(amount), "insufficient balance");
|
|
128
|
+
|
|
129
|
+
// no caller check: the destination comes from the ledger, whoever submits
|
|
130
|
+
sendUnshielded(
|
|
131
|
+
nativeToken(),
|
|
132
|
+
disclose(amount) as Uint<128>,
|
|
133
|
+
right<ContractAddress, UserAddress>(UserAddress { bytes: owner })
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
merchantBalance.insert(owner, (balance - disclose(amount)) as Uint<64>);
|
|
137
|
+
}
|