@zerc20/kohaku-plugin 0.0.0-stage → 0.1.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 +21 -0
- package/README.md +43 -2
- package/dist/index.d.ts +80 -0
- package/dist/index.js +126 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -4
- package/src/index.ts +215 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 zERC20
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,44 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @zerc20/kohaku-plugin
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
zERC20 as a [Kohaku](https://github.com/ethereum/kohaku) privacy plugin: a wallet built on `@kohaku-eth/plugins` shields
|
|
4
|
+
into zERC20, pays privately and unshields, through the `Host` it already has.
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
npm install @zerc20/kohaku-plugin @kohaku-eth/plugins viem
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { createZerc20Broadcaster, createZerc20Plugin } from '@zerc20/kohaku-plugin';
|
|
12
|
+
import { networkFromJson } from '@zerc20/wallet-sdk';
|
|
13
|
+
|
|
14
|
+
const network = networkFromJson(await (await fetch('https://v2.zerc20.io/network.json')).text());
|
|
15
|
+
const plugin = await createZerc20Plugin(host, { network, accountIndex: 0 });
|
|
16
|
+
const usdc = { __type: 'erc20', contract: '0xA0b8…eB48' } as const;
|
|
17
|
+
|
|
18
|
+
const { txns } = await plugin.prepareShield({ asset: usdc, amount }); // sent in order, before the next shield's
|
|
19
|
+
await plugin.balance([usdc]); // spendable, then `pending` (screening, or awaiting a root)
|
|
20
|
+
const op = await plugin.prepareTransfer({ asset: usdc, amount }, 'zerc20id1…');
|
|
21
|
+
await createZerc20Broadcaster(plugin).broadcast(op);
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Assets are named by their underlying ERC20 (ETH as `0xeeee…eeee`): a shield wraps it, an unshield pays it out. Named
|
|
25
|
+
by the zERC20 token itself, they move as they are.
|
|
26
|
+
|
|
27
|
+
## Why it looks like this
|
|
28
|
+
|
|
29
|
+
- **One plugin per chain.** Kohaku gives a plugin one provider, and a zERC20 account keeps a state of its own on each
|
|
30
|
+
chain, so a host opens a plugin per chain, side by side.
|
|
31
|
+
- **The host's network covers everything.** Chain reads go through `host.provider`, and every server request (the
|
|
32
|
+
relayer, the attester, the storage canister, the circuits) through `host.network.fetch`: a host's proxy or Tor
|
|
33
|
+
covers them all.
|
|
34
|
+
- **Keys come from the host's keystore**, below `m/31333'/1'/{accountIndex}'` only, so the host's mnemonic restores
|
|
35
|
+
the account. It is not the account wallet.zerc20.io opens with a passkey.
|
|
36
|
+
- **Preparing spends nothing.** `prepareTransfer` and `prepareUnshield` quote the fee and check that one JoinSplit pays
|
|
37
|
+
it; the broadcaster proves on the device, has the relayer submit, and returns once final. An interrupted broadcast
|
|
38
|
+
resumes at the next `sync()`.
|
|
39
|
+
- **The wallet underneath is `plugin.wallet`** (`@zerc20/wallet-sdk`), for receive codes, consolidation, rage exits,
|
|
40
|
+
history and view keys. `plugin.wallet.close()` lets the account go.
|
|
41
|
+
|
|
42
|
+
## License
|
|
43
|
+
|
|
44
|
+
[MIT](LICENSE)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { type AssetAmount, type ERC20AssetId, type Host, type PluginInstance, type PrivateOperation, type PublicOperation } from '@kohaku-eth/plugins';
|
|
2
|
+
import type { Broadcaster } from '@kohaku-eth/plugins/broadcaster';
|
|
3
|
+
import { type Network, type Token, Wallet } from '@zerc20/wallet-sdk';
|
|
4
|
+
import { type Address, type Hex } from 'viem';
|
|
5
|
+
/** The keystore subtree zERC20 keys come from ("ze" in ASCII as the purpose, as Kohaku's own plugins name theirs): the
|
|
6
|
+
* plugin derives nothing outside it. */
|
|
7
|
+
export declare const ZERC20_KEYSTORE_PREFIX = "m/31333'/1'";
|
|
8
|
+
export interface Zerc20PluginParameters {
|
|
9
|
+
/** The zERC20 deployment, as `networkFromJson` reads its network.json. */
|
|
10
|
+
network: Network;
|
|
11
|
+
/** Which account of the keystore's: each index is an account of its own. */
|
|
12
|
+
accountIndex: number;
|
|
13
|
+
/** Where an account with no saved state starts looking on chain: the block its key was first used at, or roughly
|
|
14
|
+
* when. Without it, an account that registered before is looked for from the chain's start, and a new one from now. */
|
|
15
|
+
birthday?: bigint | Date;
|
|
16
|
+
}
|
|
17
|
+
/** An amount of a token of the plugin's chain, named by its underlying ERC20 (ETH as `0xeeee…eeee`, as Kohaku's other
|
|
18
|
+
* plugins name it), which a shield wraps and an unshield pays out, or by the zERC20 itself, which moves as it is. */
|
|
19
|
+
export type Zerc20AssetAmount<Tag extends string | undefined = undefined> = AssetAmount<ERC20AssetId, bigint, Tag>;
|
|
20
|
+
/** The transactions a shield takes, for the host to sign and send in order before it prepares another shield of the same
|
|
21
|
+
* asset: an ERC20's approval of the amount first, which a later one would replace, then the wrap. */
|
|
22
|
+
export interface Zerc20PublicOperation extends PublicOperation {
|
|
23
|
+
txns: {
|
|
24
|
+
to: Address;
|
|
25
|
+
data: Hex;
|
|
26
|
+
value: bigint;
|
|
27
|
+
}[];
|
|
28
|
+
}
|
|
29
|
+
/** A payment or a withdrawal as quoted: nothing is spent until a broadcaster sends it. */
|
|
30
|
+
export interface Zerc20PrivateOperation extends PrivateOperation {
|
|
31
|
+
kind: 'send' | 'withdraw';
|
|
32
|
+
token: Token;
|
|
33
|
+
/** A send's recipient: a zERC20 ID, an address that opened a zERC20 wallet, or a receive code. A withdrawal's: any
|
|
34
|
+
* address. */
|
|
35
|
+
to: string;
|
|
36
|
+
amount: bigint;
|
|
37
|
+
/** The fee quoted, the most the payment may cost on top of `amount`. */
|
|
38
|
+
maxFee: bigint;
|
|
39
|
+
/** A withdrawal's payout in the underlying asset rather than the zERC20. */
|
|
40
|
+
unwrap: boolean;
|
|
41
|
+
}
|
|
42
|
+
export type Zerc20Instance = PluginInstance<string, {
|
|
43
|
+
features: {
|
|
44
|
+
prepareShield: true;
|
|
45
|
+
prepareTransfer: true;
|
|
46
|
+
prepareUnshield: true;
|
|
47
|
+
};
|
|
48
|
+
assetAmounts: {
|
|
49
|
+
input: Zerc20AssetAmount;
|
|
50
|
+
internal: Zerc20AssetAmount;
|
|
51
|
+
output: Zerc20AssetAmount;
|
|
52
|
+
/** Each asset's spendable amount, then what is on its way (screening, or awaiting a root) tagged `pending`. */
|
|
53
|
+
read: Zerc20AssetAmount<'pending'>;
|
|
54
|
+
};
|
|
55
|
+
publicOp: Zerc20PublicOperation;
|
|
56
|
+
privateOp: Zerc20PrivateOperation;
|
|
57
|
+
extras: {
|
|
58
|
+
/** Brings the account up to date with the chain and the servers, and moves operations under way along. Every call
|
|
59
|
+
* that reads the account does it first. */
|
|
60
|
+
sync(): Promise<void>;
|
|
61
|
+
/** The zERC20 wallet underneath, for what Kohaku has no call for: receive codes, consolidation, rage exits,
|
|
62
|
+
* history, a view key. `close()` lets the account go, for another plugin on the same chain. */
|
|
63
|
+
wallet: Wallet;
|
|
64
|
+
};
|
|
65
|
+
}>;
|
|
66
|
+
/** What a broadcast did: the operation's id in the wallet, and the transactions it sent. */
|
|
67
|
+
export interface Zerc20BroadcastResult {
|
|
68
|
+
id: string;
|
|
69
|
+
txHashes: Hex[];
|
|
70
|
+
}
|
|
71
|
+
export type Zerc20Broadcaster = Broadcaster<Zerc20PrivateOperation, Zerc20BroadcastResult>;
|
|
72
|
+
/** Opens account `accountIndex` of the host's keystore on the chain of its provider; the first time, it registers the
|
|
73
|
+
* account for payments. Its state lives in the host's storage, encrypted by the wallet, under the deployment's storage
|
|
74
|
+
* canister and verifier as well: the same keys on another deployment of the chain have a state of their own. An earlier
|
|
75
|
+
* plugin of the same account and chain, here or in another tab, stops: Kohaku has no call to close one, and the two
|
|
76
|
+
* would spend the same notes. */
|
|
77
|
+
export declare function createZerc20Plugin(host: Host, params: Zerc20PluginParameters): Promise<Zerc20Instance>;
|
|
78
|
+
/** Sends what `plugin` prepared: proves the JoinSplit here, has the relayer submit it, and returns once it is final.
|
|
79
|
+
* Interrupted, it resumes at the plugin's next sync. */
|
|
80
|
+
export declare function createZerc20Broadcaster(plugin: Zerc20Instance): Zerc20Broadcaster;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// @zerc20/kohaku-plugin: zERC20 as a Kohaku privacy plugin. A wallet built on @kohaku-eth/plugins shields into zERC20,
|
|
2
|
+
// pays privately and unshields through the host it already has, as it does with the other protocols.
|
|
3
|
+
//
|
|
4
|
+
// const plugin = await createZerc20Plugin(host, { network: networkFromJson(json), accountIndex: 0 });
|
|
5
|
+
// const { txns } = await plugin.prepareShield({ asset: { __type: 'erc20', contract: usdc }, amount }); // the host signs
|
|
6
|
+
// const op = await plugin.prepareTransfer({ asset, amount }, 'zerc20id1…');
|
|
7
|
+
// await createZerc20Broadcaster(plugin).broadcast(op);
|
|
8
|
+
//
|
|
9
|
+
// One plugin is one zERC20 account on the chain of the host's provider: Kohaku gives a plugin one provider, and the
|
|
10
|
+
// account's wallet on each chain keeps a state of its own, so a host opens one plugin per chain, side by side. Every
|
|
11
|
+
// chain read goes through that provider and every server request through the host's `network.fetch`, so a host's proxy
|
|
12
|
+
// or Tor covers the relayer, the attester and the storage canister too.
|
|
13
|
+
import { InsufficientBalanceError, InvalidAddressError, PluginError, UnsupportedAssetError, UnsupportedChainError, scopeKeystore, } from '@kohaku-eth/plugins';
|
|
14
|
+
import { base64 } from '@scure/base';
|
|
15
|
+
import { Wallet, WalletError, accountId, accountKey } from '@zerc20/wallet-sdk';
|
|
16
|
+
import { custom, hexToBytes, isAddress, isAddressEqual } from 'viem';
|
|
17
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
18
|
+
/** The keystore subtree zERC20 keys come from ("ze" in ASCII as the purpose, as Kohaku's own plugins name theirs): the
|
|
19
|
+
* plugin derives nothing outside it. */
|
|
20
|
+
export const ZERC20_KEYSTORE_PREFIX = "m/31333'/1'";
|
|
21
|
+
/** Opens account `accountIndex` of the host's keystore on the chain of its provider; the first time, it registers the
|
|
22
|
+
* account for payments. Its state lives in the host's storage, encrypted by the wallet, under the deployment's storage
|
|
23
|
+
* canister and verifier as well: the same keys on another deployment of the chain have a state of their own. An earlier
|
|
24
|
+
* plugin of the same account and chain, here or in another tab, stops: Kohaku has no call to close one, and the two
|
|
25
|
+
* would spend the same notes. */
|
|
26
|
+
export async function createZerc20Plugin(host, params) {
|
|
27
|
+
const chainId = await host.provider.getChainId();
|
|
28
|
+
const config = params.network.chains.find((c) => c.chainId === chainId);
|
|
29
|
+
if (!config)
|
|
30
|
+
throw new UnsupportedChainError(chainId);
|
|
31
|
+
const keystore = scopeKeystore(host.keystore, { prefixes: [ZERC20_KEYSTORE_PREFIX] });
|
|
32
|
+
// the key is the seed of every zERC20 key of the account: asked for whenever the wallet spends, and let go after
|
|
33
|
+
const seed = async () => hexToBytes(await keystore.deriveAt(`${ZERC20_KEYSTORE_PREFIX}/${params.accountIndex}'`));
|
|
34
|
+
const key = `zerc20-state:${params.network.storage.canisterId}:${chainId}:${config.verifier}:${params.accountIndex}`;
|
|
35
|
+
const request = (({ method, params }) => host.provider.request({ method, params }));
|
|
36
|
+
const wallet = await Wallet.open({
|
|
37
|
+
network: { ...params.network, chains: [config] },
|
|
38
|
+
account: privateKeyToAccount(accountKey(await seed())),
|
|
39
|
+
seed,
|
|
40
|
+
persist: {
|
|
41
|
+
load: async () => {
|
|
42
|
+
const saved = await host.storage.get(key);
|
|
43
|
+
return saved === null ? null : base64.decode(saved);
|
|
44
|
+
},
|
|
45
|
+
save: (bytes) => host.storage.set(key, base64.encode(bytes)),
|
|
46
|
+
},
|
|
47
|
+
birthday: params.birthday,
|
|
48
|
+
take: true,
|
|
49
|
+
transport: () => custom({ request }),
|
|
50
|
+
fetch: (input, init) => host.network.fetch(input, init),
|
|
51
|
+
});
|
|
52
|
+
const tokenOf = (asset) => {
|
|
53
|
+
for (const token of wallet.tokens()) {
|
|
54
|
+
if (isAddressEqual(token.underlying, asset.contract))
|
|
55
|
+
return { token, wrapped: false };
|
|
56
|
+
if (isAddressEqual(token.address, asset.contract))
|
|
57
|
+
return { token, wrapped: true };
|
|
58
|
+
}
|
|
59
|
+
throw new UnsupportedAssetError(asset);
|
|
60
|
+
};
|
|
61
|
+
/** A payment's fee now, which also checks that one JoinSplit pays it from what is spendable. */
|
|
62
|
+
const quote = async (kind, { asset, amount }, to) => {
|
|
63
|
+
await wallet.sync();
|
|
64
|
+
const { token, wrapped } = tokenOf(asset);
|
|
65
|
+
try {
|
|
66
|
+
const { fee } = await wallet.fees({ kind, token, amount });
|
|
67
|
+
return { __type: 'privateOperation', kind, token, to, amount, maxFee: fee, unwrap: !wrapped };
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
// as Kohaku's other plugins report it; settling funds and fragmented notes keep the wallet's own codes
|
|
71
|
+
if (!(e instanceof WalletError) || e.code !== 'insufficient-funds')
|
|
72
|
+
throw e;
|
|
73
|
+
throw new InsufficientBalanceError(asset, amount, (await wallet.maxAmount({ kind, token })).amount);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
return {
|
|
77
|
+
wallet,
|
|
78
|
+
sync: () => wallet.sync(),
|
|
79
|
+
instanceId: async () => accountId(wallet.address),
|
|
80
|
+
async balance(assets) {
|
|
81
|
+
await wallet.sync();
|
|
82
|
+
const balances = wallet.balances();
|
|
83
|
+
const rows = assets
|
|
84
|
+
? assets.map((asset) => ({ asset, token: tokenOf(asset).token }))
|
|
85
|
+
: balances.map((b) => ({ asset: { __type: 'erc20', contract: b.token.underlying }, token: b.token }));
|
|
86
|
+
return rows.flatMap(({ asset, token }) => {
|
|
87
|
+
const b = balances.find((b) => b.token.tokenId === token.tokenId);
|
|
88
|
+
const pending = b?.pending.reduce((sum, p) => sum + p.amount, 0n) ?? 0n;
|
|
89
|
+
return [
|
|
90
|
+
{ asset, amount: b?.spendable ?? 0n },
|
|
91
|
+
{ asset, amount: pending, tag: 'pending' },
|
|
92
|
+
];
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
async prepareShield({ asset, amount }, to) {
|
|
96
|
+
await wallet.sync();
|
|
97
|
+
const { token, wrapped } = tokenOf(asset);
|
|
98
|
+
return { __type: 'publicOperation', txns: await wallet.depositTransactions({ token, amount, to, wrapped }) };
|
|
99
|
+
},
|
|
100
|
+
prepareTransfer: (asset, to) => quote('send', asset, to),
|
|
101
|
+
async prepareUnshield(asset, to, options) {
|
|
102
|
+
// a withdrawal pays one address and calls nothing after
|
|
103
|
+
if (options?.tailCalls)
|
|
104
|
+
throw new PluginError('a zERC20 withdrawal takes no tail calls');
|
|
105
|
+
if (!isAddress(to))
|
|
106
|
+
throw new InvalidAddressError(to);
|
|
107
|
+
return quote('withdraw', asset, to);
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/** Sends what `plugin` prepared: proves the JoinSplit here, has the relayer submit it, and returns once it is final.
|
|
112
|
+
* Interrupted, it resumes at the plugin's next sync. */
|
|
113
|
+
export function createZerc20Broadcaster(plugin) {
|
|
114
|
+
return {
|
|
115
|
+
async broadcast(op) {
|
|
116
|
+
const { wallet } = plugin;
|
|
117
|
+
const { token, amount, maxFee } = op;
|
|
118
|
+
const sent = op.kind === 'send'
|
|
119
|
+
? await wallet.send({ to: op.to, token, amount, maxFee })
|
|
120
|
+
: await wallet.withdraw({ to: op.to, token, amount, maxFee, unwrap: op.unwrap });
|
|
121
|
+
await sent.wait();
|
|
122
|
+
return { id: sent.id, txHashes: [...sent.txHashes] };
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zerc20/kohaku-plugin",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "zERC20 as a Kohaku privacy plugin: shield, private transfers and unshield for wallets built on @kohaku-eth/plugins.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/zerc20io/zERC20v2.git",
|
|
10
|
+
"directory": "wallet/kohaku"
|
|
11
|
+
},
|
|
12
|
+
"main": "dist/index.js",
|
|
13
|
+
"types": "dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"src"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc -p tsconfig.json",
|
|
26
|
+
"prepack": "npm run build",
|
|
27
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
28
|
+
"test": "vitest run --config vitest.config.ts",
|
|
29
|
+
"test:e2e": "scripts/e2e.sh"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@scure/base": "^2.4.0",
|
|
33
|
+
"@zerc20/wallet-sdk": "^"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@kohaku-eth/plugins": "^0.0.1-alpha.20",
|
|
37
|
+
"viem": "^2.57.7"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@kohaku-eth/plugins": "0.0.1-alpha.20",
|
|
41
|
+
"@kohaku-eth/provider": "0.1.0-alpha.14",
|
|
42
|
+
"@types/node": "24.19.2",
|
|
43
|
+
"typescript": "6.0.3",
|
|
44
|
+
"viem": "2.57.7",
|
|
45
|
+
"vitest": "4.1.11"
|
|
46
|
+
},
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=22.12"
|
|
49
|
+
},
|
|
50
|
+
"overrides": {
|
|
51
|
+
"underscore": "1.13.8"
|
|
52
|
+
},
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// @zerc20/kohaku-plugin: zERC20 as a Kohaku privacy plugin. A wallet built on @kohaku-eth/plugins shields into zERC20,
|
|
2
|
+
// pays privately and unshields through the host it already has, as it does with the other protocols.
|
|
3
|
+
//
|
|
4
|
+
// const plugin = await createZerc20Plugin(host, { network: networkFromJson(json), accountIndex: 0 });
|
|
5
|
+
// const { txns } = await plugin.prepareShield({ asset: { __type: 'erc20', contract: usdc }, amount }); // the host signs
|
|
6
|
+
// const op = await plugin.prepareTransfer({ asset, amount }, 'zerc20id1…');
|
|
7
|
+
// await createZerc20Broadcaster(plugin).broadcast(op);
|
|
8
|
+
//
|
|
9
|
+
// One plugin is one zERC20 account on the chain of the host's provider: Kohaku gives a plugin one provider, and the
|
|
10
|
+
// account's wallet on each chain keeps a state of its own, so a host opens one plugin per chain, side by side. Every
|
|
11
|
+
// chain read goes through that provider and every server request through the host's `network.fetch`, so a host's proxy
|
|
12
|
+
// or Tor covers the relayer, the attester and the storage canister too.
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
type AssetAmount,
|
|
16
|
+
type ERC20AssetId,
|
|
17
|
+
type Host,
|
|
18
|
+
InsufficientBalanceError,
|
|
19
|
+
InvalidAddressError,
|
|
20
|
+
PluginError,
|
|
21
|
+
type PluginInstance,
|
|
22
|
+
type PrivateOperation,
|
|
23
|
+
type PublicOperation,
|
|
24
|
+
type UnshieldOptions,
|
|
25
|
+
UnsupportedAssetError,
|
|
26
|
+
UnsupportedChainError,
|
|
27
|
+
scopeKeystore,
|
|
28
|
+
} from '@kohaku-eth/plugins';
|
|
29
|
+
import type { Broadcaster } from '@kohaku-eth/plugins/broadcaster';
|
|
30
|
+
import { base64 } from '@scure/base';
|
|
31
|
+
import { type Network, type Token, Wallet, WalletError, accountId, accountKey } from '@zerc20/wallet-sdk';
|
|
32
|
+
import { type Address, type EIP1193RequestFn, type Hex, custom, hexToBytes, isAddress, isAddressEqual } from 'viem';
|
|
33
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
34
|
+
|
|
35
|
+
/** The keystore subtree zERC20 keys come from ("ze" in ASCII as the purpose, as Kohaku's own plugins name theirs): the
|
|
36
|
+
* plugin derives nothing outside it. */
|
|
37
|
+
export const ZERC20_KEYSTORE_PREFIX = "m/31333'/1'";
|
|
38
|
+
|
|
39
|
+
export interface Zerc20PluginParameters {
|
|
40
|
+
/** The zERC20 deployment, as `networkFromJson` reads its network.json. */
|
|
41
|
+
network: Network;
|
|
42
|
+
/** Which account of the keystore's: each index is an account of its own. */
|
|
43
|
+
accountIndex: number;
|
|
44
|
+
/** Where an account with no saved state starts looking on chain: the block its key was first used at, or roughly
|
|
45
|
+
* when. Without it, an account that registered before is looked for from the chain's start, and a new one from now. */
|
|
46
|
+
birthday?: bigint | Date;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** An amount of a token of the plugin's chain, named by its underlying ERC20 (ETH as `0xeeee…eeee`, as Kohaku's other
|
|
50
|
+
* plugins name it), which a shield wraps and an unshield pays out, or by the zERC20 itself, which moves as it is. */
|
|
51
|
+
export type Zerc20AssetAmount<Tag extends string | undefined = undefined> = AssetAmount<ERC20AssetId, bigint, Tag>;
|
|
52
|
+
|
|
53
|
+
/** The transactions a shield takes, for the host to sign and send in order before it prepares another shield of the same
|
|
54
|
+
* asset: an ERC20's approval of the amount first, which a later one would replace, then the wrap. */
|
|
55
|
+
export interface Zerc20PublicOperation extends PublicOperation {
|
|
56
|
+
txns: { to: Address; data: Hex; value: bigint }[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** A payment or a withdrawal as quoted: nothing is spent until a broadcaster sends it. */
|
|
60
|
+
export interface Zerc20PrivateOperation extends PrivateOperation {
|
|
61
|
+
kind: 'send' | 'withdraw';
|
|
62
|
+
token: Token;
|
|
63
|
+
/** A send's recipient: a zERC20 ID, an address that opened a zERC20 wallet, or a receive code. A withdrawal's: any
|
|
64
|
+
* address. */
|
|
65
|
+
to: string;
|
|
66
|
+
amount: bigint;
|
|
67
|
+
/** The fee quoted, the most the payment may cost on top of `amount`. */
|
|
68
|
+
maxFee: bigint;
|
|
69
|
+
/** A withdrawal's payout in the underlying asset rather than the zERC20. */
|
|
70
|
+
unwrap: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type Zerc20Instance = PluginInstance<
|
|
74
|
+
string,
|
|
75
|
+
{
|
|
76
|
+
features: { prepareShield: true; prepareTransfer: true; prepareUnshield: true };
|
|
77
|
+
assetAmounts: {
|
|
78
|
+
input: Zerc20AssetAmount;
|
|
79
|
+
internal: Zerc20AssetAmount;
|
|
80
|
+
output: Zerc20AssetAmount;
|
|
81
|
+
/** Each asset's spendable amount, then what is on its way (screening, or awaiting a root) tagged `pending`. */
|
|
82
|
+
read: Zerc20AssetAmount<'pending'>;
|
|
83
|
+
};
|
|
84
|
+
publicOp: Zerc20PublicOperation;
|
|
85
|
+
privateOp: Zerc20PrivateOperation;
|
|
86
|
+
extras: {
|
|
87
|
+
/** Brings the account up to date with the chain and the servers, and moves operations under way along. Every call
|
|
88
|
+
* that reads the account does it first. */
|
|
89
|
+
sync(): Promise<void>;
|
|
90
|
+
/** The zERC20 wallet underneath, for what Kohaku has no call for: receive codes, consolidation, rage exits,
|
|
91
|
+
* history, a view key. `close()` lets the account go, for another plugin on the same chain. */
|
|
92
|
+
wallet: Wallet;
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
>;
|
|
96
|
+
|
|
97
|
+
/** What a broadcast did: the operation's id in the wallet, and the transactions it sent. */
|
|
98
|
+
export interface Zerc20BroadcastResult {
|
|
99
|
+
id: string;
|
|
100
|
+
txHashes: Hex[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export type Zerc20Broadcaster = Broadcaster<Zerc20PrivateOperation, Zerc20BroadcastResult>;
|
|
104
|
+
|
|
105
|
+
/** Opens account `accountIndex` of the host's keystore on the chain of its provider; the first time, it registers the
|
|
106
|
+
* account for payments. Its state lives in the host's storage, encrypted by the wallet, under the deployment's storage
|
|
107
|
+
* canister and verifier as well: the same keys on another deployment of the chain have a state of their own. An earlier
|
|
108
|
+
* plugin of the same account and chain, here or in another tab, stops: Kohaku has no call to close one, and the two
|
|
109
|
+
* would spend the same notes. */
|
|
110
|
+
export async function createZerc20Plugin(host: Host, params: Zerc20PluginParameters): Promise<Zerc20Instance> {
|
|
111
|
+
const chainId = await host.provider.getChainId();
|
|
112
|
+
const config = params.network.chains.find((c) => c.chainId === chainId);
|
|
113
|
+
if (!config) throw new UnsupportedChainError(chainId);
|
|
114
|
+
const keystore = scopeKeystore(host.keystore, { prefixes: [ZERC20_KEYSTORE_PREFIX] });
|
|
115
|
+
// the key is the seed of every zERC20 key of the account: asked for whenever the wallet spends, and let go after
|
|
116
|
+
const seed = async () => hexToBytes(await keystore.deriveAt(`${ZERC20_KEYSTORE_PREFIX}/${params.accountIndex}'`));
|
|
117
|
+
const key = `zerc20-state:${params.network.storage.canisterId}:${chainId}:${config.verifier}:${params.accountIndex}`;
|
|
118
|
+
const request = (({ method, params }) => host.provider.request({ method, params })) as EIP1193RequestFn;
|
|
119
|
+
const wallet = await Wallet.open({
|
|
120
|
+
network: { ...params.network, chains: [config] },
|
|
121
|
+
account: privateKeyToAccount(accountKey(await seed())),
|
|
122
|
+
seed,
|
|
123
|
+
persist: {
|
|
124
|
+
load: async () => {
|
|
125
|
+
const saved = await host.storage.get(key);
|
|
126
|
+
return saved === null ? null : base64.decode(saved);
|
|
127
|
+
},
|
|
128
|
+
save: (bytes) => host.storage.set(key, base64.encode(bytes)),
|
|
129
|
+
},
|
|
130
|
+
birthday: params.birthday,
|
|
131
|
+
take: true,
|
|
132
|
+
transport: () => custom({ request }),
|
|
133
|
+
fetch: (input, init) => host.network.fetch(input, init),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const tokenOf = (asset: ERC20AssetId): { token: Token; wrapped: boolean } => {
|
|
137
|
+
for (const token of wallet.tokens()) {
|
|
138
|
+
if (isAddressEqual(token.underlying, asset.contract)) return { token, wrapped: false };
|
|
139
|
+
if (isAddressEqual(token.address, asset.contract)) return { token, wrapped: true };
|
|
140
|
+
}
|
|
141
|
+
throw new UnsupportedAssetError(asset);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/** A payment's fee now, which also checks that one JoinSplit pays it from what is spendable. */
|
|
145
|
+
const quote = async (
|
|
146
|
+
kind: 'send' | 'withdraw',
|
|
147
|
+
{ asset, amount }: Zerc20AssetAmount,
|
|
148
|
+
to: string,
|
|
149
|
+
): Promise<Zerc20PrivateOperation> => {
|
|
150
|
+
await wallet.sync();
|
|
151
|
+
const { token, wrapped } = tokenOf(asset);
|
|
152
|
+
try {
|
|
153
|
+
const { fee } = await wallet.fees({ kind, token, amount });
|
|
154
|
+
return { __type: 'privateOperation', kind, token, to, amount, maxFee: fee, unwrap: !wrapped };
|
|
155
|
+
} catch (e) {
|
|
156
|
+
// as Kohaku's other plugins report it; settling funds and fragmented notes keep the wallet's own codes
|
|
157
|
+
if (!(e instanceof WalletError) || e.code !== 'insufficient-funds') throw e;
|
|
158
|
+
throw new InsufficientBalanceError(asset, amount, (await wallet.maxAmount({ kind, token })).amount);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
wallet,
|
|
164
|
+
sync: () => wallet.sync(),
|
|
165
|
+
instanceId: async () => accountId(wallet.address),
|
|
166
|
+
|
|
167
|
+
async balance(assets) {
|
|
168
|
+
await wallet.sync();
|
|
169
|
+
const balances = wallet.balances();
|
|
170
|
+
const rows = assets
|
|
171
|
+
? assets.map((asset) => ({ asset, token: tokenOf(asset).token }))
|
|
172
|
+
: balances.map((b) => ({ asset: { __type: 'erc20', contract: b.token.underlying } as const, token: b.token }));
|
|
173
|
+
return rows.flatMap(({ asset, token }) => {
|
|
174
|
+
const b = balances.find((b) => b.token.tokenId === token.tokenId);
|
|
175
|
+
const pending = b?.pending.reduce((sum, p) => sum + p.amount, 0n) ?? 0n;
|
|
176
|
+
return [
|
|
177
|
+
{ asset, amount: b?.spendable ?? 0n },
|
|
178
|
+
{ asset, amount: pending, tag: 'pending' as const },
|
|
179
|
+
];
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
async prepareShield({ asset, amount }, to) {
|
|
184
|
+
await wallet.sync();
|
|
185
|
+
const { token, wrapped } = tokenOf(asset);
|
|
186
|
+
return { __type: 'publicOperation', txns: await wallet.depositTransactions({ token, amount, to, wrapped }) };
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
prepareTransfer: (asset, to) => quote('send', asset, to),
|
|
190
|
+
|
|
191
|
+
async prepareUnshield(asset, to, options?: UnshieldOptions) {
|
|
192
|
+
// a withdrawal pays one address and calls nothing after
|
|
193
|
+
if (options?.tailCalls) throw new PluginError('a zERC20 withdrawal takes no tail calls');
|
|
194
|
+
if (!isAddress(to)) throw new InvalidAddressError(to);
|
|
195
|
+
return quote('withdraw', asset, to);
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Sends what `plugin` prepared: proves the JoinSplit here, has the relayer submit it, and returns once it is final.
|
|
201
|
+
* Interrupted, it resumes at the plugin's next sync. */
|
|
202
|
+
export function createZerc20Broadcaster(plugin: Zerc20Instance): Zerc20Broadcaster {
|
|
203
|
+
return {
|
|
204
|
+
async broadcast(op) {
|
|
205
|
+
const { wallet } = plugin;
|
|
206
|
+
const { token, amount, maxFee } = op;
|
|
207
|
+
const sent =
|
|
208
|
+
op.kind === 'send'
|
|
209
|
+
? await wallet.send({ to: op.to, token, amount, maxFee })
|
|
210
|
+
: await wallet.withdraw({ to: op.to as Address, token, amount, maxFee, unwrap: op.unwrap });
|
|
211
|
+
await sent.wait();
|
|
212
|
+
return { id: sent.id, txHashes: [...sent.txHashes] };
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|