@getcofund/protocol 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 +414 -2
- package/dist/btc/index.d.ts +12 -0
- package/dist/btc/index.js +13 -0
- package/dist/btc/psbt.d.ts +121 -0
- package/dist/btc/psbt.js +377 -0
- package/dist/btc/reader.d.ts +42 -0
- package/dist/btc/reader.js +85 -0
- package/dist/btc/select.d.ts +48 -0
- package/dist/btc/select.js +59 -0
- package/dist/btc/spend-rules.d.ts +24 -0
- package/dist/btc/spend-rules.js +43 -0
- package/dist/btc/spend.d.ts +136 -0
- package/dist/btc/spend.js +180 -0
- package/dist/btc/types.d.ts +65 -0
- package/dist/btc/types.js +62 -0
- package/dist/btc/vault/bip341-nums.d.ts +35 -0
- package/dist/btc/vault/bip341-nums.js +121 -0
- package/dist/btc/vault/build.d.ts +5 -0
- package/dist/btc/vault/build.js +136 -0
- package/dist/btc/vault/canonical-order.d.ts +10 -0
- package/dist/btc/vault/canonical-order.js +18 -0
- package/dist/btc/vault/from-contract-id.d.ts +9 -0
- package/dist/btc/vault/from-contract-id.js +74 -0
- package/dist/btc/vault/helpers.d.ts +73 -0
- package/dist/btc/vault/helpers.js +129 -0
- package/dist/btc/vault/index.d.ts +21 -0
- package/dist/btc/vault/index.js +21 -0
- package/dist/btc/vault/limits.d.ts +9 -0
- package/dist/btc/vault/limits.js +9 -0
- package/dist/btc/vault/participant-order.d.ts +69 -0
- package/dist/btc/vault/participant-order.js +135 -0
- package/dist/btc/vault/script.d.ts +13 -0
- package/dist/btc/vault/script.js +39 -0
- package/dist/btc/vault/spend.d.ts +63 -0
- package/dist/btc/vault/spend.js +93 -0
- package/dist/btc/vault/tx-size.d.ts +66 -0
- package/dist/btc/vault/tx-size.js +71 -0
- package/dist/btc/vault/types.d.ts +49 -0
- package/dist/btc/vault/types.js +1 -0
- package/dist/btc/vault/vault-policy.d.ts +109 -0
- package/dist/btc/vault/vault-policy.js +213 -0
- package/dist/btc/vault/vaultContractUtil.d.ts +80 -0
- package/dist/btc/vault/vaultContractUtil.js +87 -0
- package/dist/btc/vault/xpub.d.ts +28 -0
- package/dist/btc/vault/xpub.js +41 -0
- package/dist/btc/witness.d.ts +101 -0
- package/dist/btc/witness.js +439 -0
- package/dist/btc-policies/activation.d.ts +76 -0
- package/dist/btc-policies/activation.js +153 -0
- package/dist/btc-policies/index.d.ts +7 -0
- package/dist/btc-policies/index.js +8 -0
- package/dist/btc-policies/policy.d.ts +55 -0
- package/dist/btc-policies/policy.js +210 -0
- package/dist/evm/abis/index.d.ts +10 -0
- package/dist/evm/abis/index.js +11 -0
- package/dist/evm/abis/policies.d.ts +279 -0
- package/dist/evm/abis/policies.js +363 -0
- package/dist/evm/abis/sponsor-guard.d.ts +201 -0
- package/dist/evm/abis/sponsor-guard.js +263 -0
- package/dist/evm/abis/state.d.ts +989 -0
- package/dist/evm/abis/state.js +1290 -0
- package/dist/evm/abis/users.d.ts +408 -0
- package/dist/evm/abis/users.js +532 -0
- package/dist/evm/abis/vault-factory.d.ts +92 -0
- package/dist/evm/abis/vault-factory.js +126 -0
- package/dist/evm/abis/vault.d.ts +343 -0
- package/dist/evm/abis/vault.js +444 -0
- package/dist/evm/address.d.ts +2 -0
- package/dist/evm/address.js +4 -0
- package/dist/evm/calls/common.d.ts +30 -0
- package/dist/evm/calls/common.js +51 -0
- package/dist/evm/calls/index.d.ts +11 -0
- package/dist/evm/calls/index.js +4 -0
- package/dist/evm/calls/policies.d.ts +25 -0
- package/dist/evm/calls/policies.js +173 -0
- package/dist/evm/calls/users.d.ts +61 -0
- package/dist/evm/calls/users.js +353 -0
- package/dist/evm/calls/vault.d.ts +25 -0
- package/dist/evm/calls/vault.js +184 -0
- package/dist/evm/chain.d.ts +74 -0
- package/dist/evm/chain.js +48 -0
- package/dist/evm/company-name.d.ts +46 -0
- package/dist/evm/company-name.js +64 -0
- package/dist/evm/deployment.d.ts +47 -0
- package/dist/evm/deployment.js +110 -0
- package/dist/evm/eip712.d.ts +271 -0
- package/dist/evm/eip712.js +224 -0
- package/dist/evm/index.d.ts +20 -0
- package/dist/evm/index.js +21 -0
- package/dist/evm/reads.d.ts +64 -0
- package/dist/evm/reads.js +131 -0
- package/dist/evm/signatures.d.ts +57 -0
- package/dist/evm/signatures.js +105 -0
- package/dist/evm/simulate/balances.d.ts +76 -0
- package/dist/evm/simulate/balances.js +201 -0
- package/dist/evm/simulate/describe.d.ts +79 -0
- package/dist/evm/simulate/describe.js +188 -0
- package/dist/evm/simulate/index.d.ts +12 -0
- package/dist/evm/simulate/index.js +13 -0
- package/dist/evm/simulate/own-node.d.ts +62 -0
- package/dist/evm/simulate/own-node.js +122 -0
- package/dist/evm/simulate/request.d.ts +86 -0
- package/dist/evm/simulate/request.js +235 -0
- package/dist/evm/simulate/revert.d.ts +29 -0
- package/dist/evm/simulate/revert.js +192 -0
- package/dist/evm/simulate/run.d.ts +51 -0
- package/dist/evm/simulate/run.js +214 -0
- package/dist/evm/stocks/decode-stock-swap.d.ts +32 -0
- package/dist/evm/stocks/decode-stock-swap.js +79 -0
- package/dist/evm/stocks/encode-swap.d.ts +45 -0
- package/dist/evm/stocks/encode-swap.js +45 -0
- package/dist/evm/stocks/index.d.ts +9 -0
- package/dist/evm/stocks/index.js +10 -0
- package/dist/evm/stocks/quote.d.ts +19 -0
- package/dist/evm/stocks/quote.js +46 -0
- package/dist/evm/stocks/stock-pools.d.ts +61 -0
- package/dist/evm/stocks/stock-pools.js +179 -0
- package/dist/evm/submitters.d.ts +32 -0
- package/dist/evm/submitters.js +105 -0
- package/dist/evm/types.d.ts +146 -0
- package/dist/evm/types.js +128 -0
- package/dist/keys/index.d.ts +6 -0
- package/dist/keys/index.js +7 -0
- package/dist/keys/keys.d.ts +63 -0
- package/dist/keys/keys.js +80 -0
- package/dist/keys/xpub-ownership.d.ts +10 -0
- package/dist/keys/xpub-ownership.js +13 -0
- package/package.json +60 -3
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { zeroAddress } from "viem";
|
|
2
|
+
import { policiesAbi } from "./abis/policies.js";
|
|
3
|
+
import { stateAbi } from "./abis/state.js";
|
|
4
|
+
import { usersAbi } from "./abis/users.js";
|
|
5
|
+
import { vaultFactoryAbi } from "./abis/vault-factory.js";
|
|
6
|
+
import { readContract } from "./chain.js";
|
|
7
|
+
/** `State.getUser`: null when `userId` was never seated in the cofund. */
|
|
8
|
+
export async function readUser(reader, deployment, clientId, userId) {
|
|
9
|
+
const user = await readContract(reader, {
|
|
10
|
+
address: deployment.state,
|
|
11
|
+
abi: stateAbi,
|
|
12
|
+
functionName: "getUser",
|
|
13
|
+
args: [clientId, userId],
|
|
14
|
+
});
|
|
15
|
+
return user.addr === zeroAddress
|
|
16
|
+
? null
|
|
17
|
+
: {
|
|
18
|
+
address: user.addr,
|
|
19
|
+
position: user.position,
|
|
20
|
+
isAdmin: user.isAdmin,
|
|
21
|
+
active: user.active,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** `State.getUserIdByAddress`: the user id `address` holds in the cofund, active or not; null when none. */
|
|
25
|
+
export async function readUserIdByAddress(reader, deployment, clientId, address) {
|
|
26
|
+
const userId = await readContract(reader, {
|
|
27
|
+
address: deployment.state,
|
|
28
|
+
abi: stateAbi,
|
|
29
|
+
functionName: "getUserIdByAddress",
|
|
30
|
+
args: [address, clientId],
|
|
31
|
+
});
|
|
32
|
+
return userId === "" ? null : userId;
|
|
33
|
+
}
|
|
34
|
+
/** `State.getPolicy`: null when no policy was ever activated under `policyId`. */
|
|
35
|
+
export async function readPolicy(reader, deployment, clientId, policyId) {
|
|
36
|
+
const policy = await readContract(reader, {
|
|
37
|
+
address: deployment.state,
|
|
38
|
+
abi: stateAbi,
|
|
39
|
+
functionName: "getPolicy",
|
|
40
|
+
args: [clientId, policyId],
|
|
41
|
+
});
|
|
42
|
+
return policy.title === ""
|
|
43
|
+
? null
|
|
44
|
+
: {
|
|
45
|
+
active: policy.active,
|
|
46
|
+
title: policy.title,
|
|
47
|
+
policyType: policy.policyType,
|
|
48
|
+
signerIds: [...policy.signerIds],
|
|
49
|
+
threshold: Number(policy.threshold),
|
|
50
|
+
transaction: { ...policy.transactionDetails },
|
|
51
|
+
transfer: {
|
|
52
|
+
maxAmount: policy.transferDetails.maxAmount,
|
|
53
|
+
token: policy.transferDetails.token,
|
|
54
|
+
recipients: [...policy.transferDetails.recipients],
|
|
55
|
+
},
|
|
56
|
+
isTransaction: policy.isTransaction,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** `State.clientVaults`: the cofund's vault and its subscription tier; null before it is created. */
|
|
60
|
+
export async function readClientVault(reader, deployment, clientId) {
|
|
61
|
+
const [vault, tier] = await readContract(reader, {
|
|
62
|
+
address: deployment.state,
|
|
63
|
+
abi: stateAbi,
|
|
64
|
+
functionName: "clientVaults",
|
|
65
|
+
args: [clientId],
|
|
66
|
+
});
|
|
67
|
+
return vault === zeroAddress ? null : { vault, tier };
|
|
68
|
+
}
|
|
69
|
+
/** `State.onboardingComplete`: whether the cofund's onboarding phase is closed. */
|
|
70
|
+
export async function readOnboardingComplete(reader, deployment, clientId) {
|
|
71
|
+
return await readContract(reader, {
|
|
72
|
+
address: deployment.state,
|
|
73
|
+
abi: stateAbi,
|
|
74
|
+
functionName: "onboardingComplete",
|
|
75
|
+
args: [clientId],
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/** `State.getActiveAdmins`: the cofund's active admins. */
|
|
79
|
+
export async function readActiveAdmins(reader, deployment, clientId) {
|
|
80
|
+
return Number(await readContract(reader, {
|
|
81
|
+
address: deployment.state,
|
|
82
|
+
abi: stateAbi,
|
|
83
|
+
functionName: "getActiveAdmins",
|
|
84
|
+
args: [clientId],
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
/** `State.helperContracts(kind)`: zero when none is registered. */
|
|
88
|
+
export async function readHelperContract(reader, deployment, kind) {
|
|
89
|
+
return await readContract(reader, {
|
|
90
|
+
address: deployment.state,
|
|
91
|
+
abi: stateAbi,
|
|
92
|
+
functionName: "helperContracts",
|
|
93
|
+
args: [kind],
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/** `State.getClientContractAuthID`: whether `authId` is spent for the cofund (one use, across every helper and the vault). */
|
|
97
|
+
export async function readAuthIdSpent(reader, deployment, clientId, authId) {
|
|
98
|
+
return await readContract(reader, {
|
|
99
|
+
address: deployment.state,
|
|
100
|
+
abi: stateAbi,
|
|
101
|
+
functionName: "getClientContractAuthID",
|
|
102
|
+
args: [clientId, authId],
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/** `Users.requiredAdminInviteSignatures`: admin signatures an admin seat needs, the inviter's included. */
|
|
106
|
+
export async function readRequiredAdminInviteSignatures(reader, deployment, clientId) {
|
|
107
|
+
return Number(await readContract(reader, {
|
|
108
|
+
address: deployment.users,
|
|
109
|
+
abi: usersAbi,
|
|
110
|
+
functionName: "requiredAdminInviteSignatures",
|
|
111
|
+
args: [clientId],
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
/** `Policies.requiredPolicyActivationSignatures`: admin signatures activating a policy of `threshold` needs. */
|
|
115
|
+
export async function readRequiredPolicyActivationSignatures(reader, deployment, clientId, threshold) {
|
|
116
|
+
return Number(await readContract(reader, {
|
|
117
|
+
address: deployment.policies,
|
|
118
|
+
abi: policiesAbi,
|
|
119
|
+
functionName: "requiredPolicyActivationSignatures",
|
|
120
|
+
args: [clientId, BigInt(threshold)],
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
/** `VaultFactory.predictVault`: the address a cofund's vault has, or will have once it is created. */
|
|
124
|
+
export async function readPredictedVault(reader, deployment, clientId) {
|
|
125
|
+
return await readContract(reader, {
|
|
126
|
+
address: deployment.vaults,
|
|
127
|
+
abi: vaultFactoryAbi,
|
|
128
|
+
functionName: "predictVault",
|
|
129
|
+
args: [clientId],
|
|
130
|
+
});
|
|
131
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type Address, type Hex } from "viem";
|
|
2
|
+
/**
|
|
3
|
+
* Recovering a signer the way the contracts do, and refusing a call the way
|
|
4
|
+
* they would.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* A call the contract would revert. `reason` is the error it would revert
|
|
8
|
+
* with (`ThresholdNotMet`, `InvalidSignature`, ...), the message says why.
|
|
9
|
+
*/
|
|
10
|
+
export declare class CallRefusedError extends Error {
|
|
11
|
+
readonly reason: string;
|
|
12
|
+
constructor(reason: string, detail: string);
|
|
13
|
+
}
|
|
14
|
+
/** `throw new CallRefusedError(...)`, as an expression. */
|
|
15
|
+
export declare function refuse(reason: string, detail: string): never;
|
|
16
|
+
/** Why OpenZeppelin's `ECDSA.tryRecover` fails a signature: its `RecoverError`. */
|
|
17
|
+
export type RecoverFailure = "InvalidSignatureLength" | "InvalidSignatureS" | "InvalidSignature";
|
|
18
|
+
/**
|
|
19
|
+
* The address `signature` recovers to over `digest`, as OpenZeppelin's
|
|
20
|
+
* `ECDSA.tryRecover` decides it: 65 bytes `r ‖ s ‖ v`, `v` 27 or 28, `s` in
|
|
21
|
+
* the lower half, and a point `ecrecover` accepts. EIP-2098 compact
|
|
22
|
+
* signatures are refused, as the contracts refuse them.
|
|
23
|
+
*/
|
|
24
|
+
export declare function tryRecover(digest: Hex, signature: string): Promise<{
|
|
25
|
+
signer: Address;
|
|
26
|
+
} | {
|
|
27
|
+
failure: RecoverFailure;
|
|
28
|
+
}>;
|
|
29
|
+
/**
|
|
30
|
+
* How a contract names a signature `tryRecover` fails: the helpers map every
|
|
31
|
+
* failure to their own `InvalidSignature()`; the vault uses `ECDSA.recover`,
|
|
32
|
+
* which reverts with OpenZeppelin's own `ECDSA…` errors.
|
|
33
|
+
*/
|
|
34
|
+
export type RecoverErrors = "helper" | "vault";
|
|
35
|
+
/** The signer of `signature` over `digest`, or the contract's refusal. `what` names the signature. */
|
|
36
|
+
export declare function recoverSigner(digest: Hex, signature: string, { what, errors }: {
|
|
37
|
+
what: string;
|
|
38
|
+
errors?: RecoverErrors;
|
|
39
|
+
}): Promise<Address>;
|
|
40
|
+
/** A signature and the address it recovers to. */
|
|
41
|
+
export type SignedBy = {
|
|
42
|
+
signature: Hex;
|
|
43
|
+
signer: Address;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* `signatures` in the order the contracts walk them: strictly ascending by
|
|
47
|
+
* recovered address, which is also their uniqueness check (`signer <= prev`
|
|
48
|
+
* reverts `InvalidSignature`). The order is derived here, never assumed from
|
|
49
|
+
* the order the signatures were collected in; two from one signer are
|
|
50
|
+
* refused.
|
|
51
|
+
*/
|
|
52
|
+
export declare function ascendingSigners(digest: Hex, signatures: readonly string[], { what, errors }: {
|
|
53
|
+
what: string;
|
|
54
|
+
errors?: RecoverErrors;
|
|
55
|
+
}): Promise<SignedBy[]>;
|
|
56
|
+
/** Address order as Solidity compares `address` values: numerically. */
|
|
57
|
+
export declare function compareAddresses(a: string, b: string): number;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { recoverAddress } from "viem";
|
|
2
|
+
/**
|
|
3
|
+
* Recovering a signer the way the contracts do, and refusing a call the way
|
|
4
|
+
* they would.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* A call the contract would revert. `reason` is the error it would revert
|
|
8
|
+
* with (`ThresholdNotMet`, `InvalidSignature`, ...), the message says why.
|
|
9
|
+
*/
|
|
10
|
+
export class CallRefusedError extends Error {
|
|
11
|
+
reason;
|
|
12
|
+
constructor(reason, detail) {
|
|
13
|
+
super(`${reason}: ${detail}`);
|
|
14
|
+
this.name = "CallRefusedError";
|
|
15
|
+
this.reason = reason;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** `throw new CallRefusedError(...)`, as an expression. */
|
|
19
|
+
export function refuse(reason, detail) {
|
|
20
|
+
throw new CallRefusedError(reason, detail);
|
|
21
|
+
}
|
|
22
|
+
/** 65 bytes of hex: `r ‖ s ‖ v`. */
|
|
23
|
+
const SIGNATURE = /^0x[0-9a-fA-F]{130}$/;
|
|
24
|
+
/** secp256k1's n / 2: OpenZeppelin's ECDSA refuses an `s` above it (malleability). */
|
|
25
|
+
const HALF_ORDER = 0x7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0n;
|
|
26
|
+
/**
|
|
27
|
+
* The address `signature` recovers to over `digest`, as OpenZeppelin's
|
|
28
|
+
* `ECDSA.tryRecover` decides it: 65 bytes `r ‖ s ‖ v`, `v` 27 or 28, `s` in
|
|
29
|
+
* the lower half, and a point `ecrecover` accepts. EIP-2098 compact
|
|
30
|
+
* signatures are refused, as the contracts refuse them.
|
|
31
|
+
*/
|
|
32
|
+
export async function tryRecover(digest, signature) {
|
|
33
|
+
if (!SIGNATURE.test(signature)) {
|
|
34
|
+
return { failure: "InvalidSignatureLength" };
|
|
35
|
+
}
|
|
36
|
+
const s = BigInt(`0x${signature.slice(66, 130)}`);
|
|
37
|
+
const v = Number.parseInt(signature.slice(130, 132), 16);
|
|
38
|
+
if (s > HALF_ORDER) {
|
|
39
|
+
return { failure: "InvalidSignatureS" };
|
|
40
|
+
}
|
|
41
|
+
if (v !== 27 && v !== 28) {
|
|
42
|
+
return { failure: "InvalidSignature" };
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
return {
|
|
46
|
+
signer: await recoverAddress({
|
|
47
|
+
hash: digest,
|
|
48
|
+
signature: signature,
|
|
49
|
+
}),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return { failure: "InvalidSignature" };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const VAULT_ERRORS = {
|
|
57
|
+
InvalidSignatureLength: "ECDSAInvalidSignatureLength",
|
|
58
|
+
InvalidSignatureS: "ECDSAInvalidSignatureS",
|
|
59
|
+
InvalidSignature: "ECDSAInvalidSignature",
|
|
60
|
+
};
|
|
61
|
+
/** The signer of `signature` over `digest`, or the contract's refusal. `what` names the signature. */
|
|
62
|
+
export async function recoverSigner(digest, signature, { what, errors = "helper" }) {
|
|
63
|
+
const recovered = await tryRecover(digest, signature);
|
|
64
|
+
if ("failure" in recovered) {
|
|
65
|
+
const detail = {
|
|
66
|
+
InvalidSignatureLength: `${what} is not a 65-byte signature`,
|
|
67
|
+
InvalidSignatureS: `${what} has a high s value (malleable)`,
|
|
68
|
+
InvalidSignature: `${what} does not recover to an address`,
|
|
69
|
+
}[recovered.failure];
|
|
70
|
+
return refuse(errors === "vault" ? VAULT_ERRORS[recovered.failure] : "InvalidSignature", detail);
|
|
71
|
+
}
|
|
72
|
+
return recovered.signer;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* `signatures` in the order the contracts walk them: strictly ascending by
|
|
76
|
+
* recovered address, which is also their uniqueness check (`signer <= prev`
|
|
77
|
+
* reverts `InvalidSignature`). The order is derived here, never assumed from
|
|
78
|
+
* the order the signatures were collected in; two from one signer are
|
|
79
|
+
* refused.
|
|
80
|
+
*/
|
|
81
|
+
export async function ascendingSigners(digest, signatures, { what, errors = "helper" }) {
|
|
82
|
+
const signed = await Promise.all(signatures.map(async (signature, i) => ({
|
|
83
|
+
signature: signature,
|
|
84
|
+
signer: await recoverSigner(digest, signature, {
|
|
85
|
+
what: `${what} ${i + 1}`,
|
|
86
|
+
errors,
|
|
87
|
+
}),
|
|
88
|
+
})));
|
|
89
|
+
signed.sort((a, b) => compareAddresses(a.signer, b.signer));
|
|
90
|
+
for (let i = 1; i < signed.length; i += 1) {
|
|
91
|
+
if (compareAddresses(signed[i - 1].signer, signed[i].signer) === 0) {
|
|
92
|
+
refuse("InvalidSignature", `${signed[i].signer} signed twice`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return signed;
|
|
96
|
+
}
|
|
97
|
+
/** Address order as Solidity compares `address` values: numerically. */
|
|
98
|
+
export function compareAddresses(a, b) {
|
|
99
|
+
const x = BigInt(a);
|
|
100
|
+
const y = BigInt(b);
|
|
101
|
+
if (x === y) {
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
return x < y ? -1 : 1;
|
|
105
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type BalanceChange } from "../types.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Logs to balances: which (asset, holder) pairs a call moves, netted, the
|
|
5
|
+
* vault's first, and the tokens' own claims about themselves.
|
|
6
|
+
*/
|
|
7
|
+
/** How `traceTransfers` and this module name the native coin. */
|
|
8
|
+
export declare const NATIVE_ASSET = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
|
|
9
|
+
/** The most (token, holder) balances one simulation reads exactly. */
|
|
10
|
+
export declare const MAX_SIMULATED_BALANCES = 50;
|
|
11
|
+
/** A JSON-RPC quantity: 0x and hex digits. */
|
|
12
|
+
export declare const RpcQuantity: z.ZodString;
|
|
13
|
+
/** A log as a node returns it. */
|
|
14
|
+
export declare const RpcLogSchema: z.ZodObject<{
|
|
15
|
+
address: z.ZodString;
|
|
16
|
+
topics: z.ZodArray<z.ZodString>;
|
|
17
|
+
data: z.ZodString;
|
|
18
|
+
}, z.core.$strip>;
|
|
19
|
+
export type BalanceLog = z.infer<typeof RpcLogSchema>;
|
|
20
|
+
/** One holder's net movement in one asset (`NATIVE_ASSET` for the native coin), lowercased. */
|
|
21
|
+
export type NetBalance = {
|
|
22
|
+
asset: string;
|
|
23
|
+
holder: string;
|
|
24
|
+
delta: bigint;
|
|
25
|
+
};
|
|
26
|
+
/** Whose balance it is: the vault, the call's wrapper, or anyone else. */
|
|
27
|
+
export type HolderRoles = {
|
|
28
|
+
vault: string;
|
|
29
|
+
wrapper: string;
|
|
30
|
+
};
|
|
31
|
+
/** A token's name and decimals, and whether they are Cofund's listing or its own claim. */
|
|
32
|
+
export type TokenInfo = {
|
|
33
|
+
symbol: string | null;
|
|
34
|
+
decimals: number | null;
|
|
35
|
+
listed: boolean;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Every balance the logs move, netted per (asset, holder) in first-seen order:
|
|
39
|
+
* an ERC-20 `Transfer` (3 topics, one word of data) or `traceTransfers`'
|
|
40
|
+
* native one moves both ends (not the zero address: a mint or burn); a
|
|
41
|
+
* WETH-style `Deposit` mints to its `dst` and `Withdrawal` burns from its
|
|
42
|
+
* `src`. An ERC-721 `Transfer` (4 topics) or an ERC-1155 event leaves the
|
|
43
|
+
* result `incomplete`: NFTs are not counted. Zero nets are kept here: a
|
|
44
|
+
* touched balance can still change (a rebasing token), which the exact pass
|
|
45
|
+
* reads.
|
|
46
|
+
*/
|
|
47
|
+
export declare function decodeBalanceLogs(logs: BalanceLog[]): {
|
|
48
|
+
balances: NetBalance[];
|
|
49
|
+
incomplete: boolean;
|
|
50
|
+
};
|
|
51
|
+
/** The vault's first, then the wrapper's, then everyone else's, each as they came. */
|
|
52
|
+
export declare function byRole<T extends {
|
|
53
|
+
role: BalanceChange["role"];
|
|
54
|
+
}>(items: T[]): T[];
|
|
55
|
+
/** The most balances read exactly: the vault's and the wrapper's before anyone else's. */
|
|
56
|
+
export declare function capBalances(balances: NetBalance[], roles: HolderRoles): {
|
|
57
|
+
kept: NetBalance[];
|
|
58
|
+
dropped: boolean;
|
|
59
|
+
};
|
|
60
|
+
/** The changes to report: non-zero nets, each with its role and asset, the vault first. */
|
|
61
|
+
export declare function toChanges(balances: NetBalance[], roles: HolderRoles, token: (address: string) => TokenInfo | null): BalanceChange[];
|
|
62
|
+
/** The `balanceOf`, `symbol()` and `decimals()` reads a pass makes. */
|
|
63
|
+
export declare const tokenReads: {
|
|
64
|
+
balanceOf: (holder: string) => `0x${string}`;
|
|
65
|
+
symbol: `0x${string}`;
|
|
66
|
+
decimals: `0x${string}`;
|
|
67
|
+
};
|
|
68
|
+
/** A token's own `symbol()`: a plain symbol, or null (a bytes32 one, NUL-padded, is read too). */
|
|
69
|
+
export declare function claimedSymbol(data: string): string | null;
|
|
70
|
+
/** A token's own `decimals()`, or null when it is not one. */
|
|
71
|
+
export declare function claimedDecimals(data: string): number | null;
|
|
72
|
+
/** A balance read's value, or null when the read failed or is not a word. */
|
|
73
|
+
export declare function balanceOf(result: {
|
|
74
|
+
status: string;
|
|
75
|
+
returnData: string;
|
|
76
|
+
}): bigint | null;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { decodeFunctionResult, encodeFunctionData, getAddress, hexToBytes, parseAbi, size, sliceHex, toEventSelector, zeroAddress, } from "viem";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { sameAddress } from "../address.js";
|
|
4
|
+
import { Address, Bytes32, Hex, TokenDecimals, TokenSymbol, } from "../types.js";
|
|
5
|
+
/**
|
|
6
|
+
* Logs to balances: which (asset, holder) pairs a call moves, netted, the
|
|
7
|
+
* vault's first, and the tokens' own claims about themselves.
|
|
8
|
+
*/
|
|
9
|
+
/** How `traceTransfers` and this module name the native coin. */
|
|
10
|
+
export const NATIVE_ASSET = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
|
|
11
|
+
/** The most (token, holder) balances one simulation reads exactly. */
|
|
12
|
+
export const MAX_SIMULATED_BALANCES = 50;
|
|
13
|
+
const TRANSFER = toEventSelector("Transfer(address,address,uint256)");
|
|
14
|
+
const DEPOSIT = toEventSelector("Deposit(address,uint256)");
|
|
15
|
+
const WITHDRAWAL = toEventSelector("Withdrawal(address,uint256)");
|
|
16
|
+
const TRANSFER_SINGLE = toEventSelector("TransferSingle(address,address,address,uint256,uint256)");
|
|
17
|
+
const TRANSFER_BATCH = toEventSelector("TransferBatch(address,address,address,uint256[],uint256[])");
|
|
18
|
+
const ERC20 = parseAbi([
|
|
19
|
+
"function balanceOf(address) view returns (uint256)",
|
|
20
|
+
"function symbol() view returns (string)",
|
|
21
|
+
"function decimals() view returns (uint8)",
|
|
22
|
+
]);
|
|
23
|
+
/** Invalid UTF-8 is no symbol: refused, not replaced. */
|
|
24
|
+
const STRICT_UTF8 = new TextDecoder("utf-8", { fatal: true });
|
|
25
|
+
/** A JSON-RPC quantity: 0x and hex digits. */
|
|
26
|
+
export const RpcQuantity = z
|
|
27
|
+
.string()
|
|
28
|
+
.regex(/^0x[0-9a-fA-F]{1,64}$/, "a hex quantity");
|
|
29
|
+
/** A log as a node returns it. */
|
|
30
|
+
export const RpcLogSchema = z.object({
|
|
31
|
+
address: Address,
|
|
32
|
+
topics: z.array(Bytes32).max(4),
|
|
33
|
+
data: Hex,
|
|
34
|
+
});
|
|
35
|
+
const topicAddress = (topic) => sliceHex(topic, 12).toLowerCase();
|
|
36
|
+
const isWord = (data) => size(data) === 32;
|
|
37
|
+
/**
|
|
38
|
+
* Every balance the logs move, netted per (asset, holder) in first-seen order:
|
|
39
|
+
* an ERC-20 `Transfer` (3 topics, one word of data) or `traceTransfers`'
|
|
40
|
+
* native one moves both ends (not the zero address: a mint or burn); a
|
|
41
|
+
* WETH-style `Deposit` mints to its `dst` and `Withdrawal` burns from its
|
|
42
|
+
* `src`. An ERC-721 `Transfer` (4 topics) or an ERC-1155 event leaves the
|
|
43
|
+
* result `incomplete`: NFTs are not counted. Zero nets are kept here: a
|
|
44
|
+
* touched balance can still change (a rebasing token), which the exact pass
|
|
45
|
+
* reads.
|
|
46
|
+
*/
|
|
47
|
+
export function decodeBalanceLogs(logs) {
|
|
48
|
+
const net = new Map();
|
|
49
|
+
let incomplete = false;
|
|
50
|
+
for (const log of logs) {
|
|
51
|
+
const moves = logMoves(log);
|
|
52
|
+
if (moves === "nft") {
|
|
53
|
+
incomplete = true;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
for (const { asset, holder, delta } of moves) {
|
|
57
|
+
if (holder === zeroAddress) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const key = `${asset}|${holder}`;
|
|
61
|
+
const entry = net.get(key) ?? { asset, holder, delta: 0n };
|
|
62
|
+
entry.delta += delta;
|
|
63
|
+
net.set(key, entry);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { balances: [...net.values()], incomplete };
|
|
67
|
+
}
|
|
68
|
+
/** The balances one log moves; "nft" for an NFT's, none for any other log. */
|
|
69
|
+
function logMoves(log) {
|
|
70
|
+
const [topic, ...indexed] = log.topics.map((t) => t.toLowerCase());
|
|
71
|
+
const asset = log.address.toLowerCase();
|
|
72
|
+
if (topic === TRANSFER_SINGLE || topic === TRANSFER_BATCH) {
|
|
73
|
+
return "nft";
|
|
74
|
+
}
|
|
75
|
+
if (topic === TRANSFER && indexed.length === 3) {
|
|
76
|
+
return "nft";
|
|
77
|
+
}
|
|
78
|
+
if (!isWord(log.data)) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
const amount = BigInt(log.data);
|
|
82
|
+
if (topic === TRANSFER && indexed.length === 2) {
|
|
83
|
+
return [
|
|
84
|
+
{ asset, holder: topicAddress(indexed[0]), delta: -amount },
|
|
85
|
+
{ asset, holder: topicAddress(indexed[1]), delta: amount },
|
|
86
|
+
];
|
|
87
|
+
}
|
|
88
|
+
const mint = topic === DEPOSIT ? amount : null;
|
|
89
|
+
const delta = topic === WITHDRAWAL ? -amount : mint;
|
|
90
|
+
return delta !== null && indexed.length === 1
|
|
91
|
+
? [{ asset, holder: topicAddress(indexed[0]), delta }]
|
|
92
|
+
: [];
|
|
93
|
+
}
|
|
94
|
+
const roleOf = (holder, roles) => {
|
|
95
|
+
if (sameAddress(holder, roles.vault)) {
|
|
96
|
+
return "vault";
|
|
97
|
+
}
|
|
98
|
+
return sameAddress(holder, roles.wrapper) ? "wrapper" : "other";
|
|
99
|
+
};
|
|
100
|
+
const ROLE_ORDER = {
|
|
101
|
+
vault: 0,
|
|
102
|
+
wrapper: 1,
|
|
103
|
+
other: 2,
|
|
104
|
+
};
|
|
105
|
+
/** The vault's first, then the wrapper's, then everyone else's, each as they came. */
|
|
106
|
+
export function byRole(items) {
|
|
107
|
+
return items
|
|
108
|
+
.map((item, i) => ({ item, i }))
|
|
109
|
+
.sort((a, b) => ROLE_ORDER[a.item.role] - ROLE_ORDER[b.item.role] || a.i - b.i)
|
|
110
|
+
.map(({ item }) => item);
|
|
111
|
+
}
|
|
112
|
+
/** The most balances read exactly: the vault's and the wrapper's before anyone else's. */
|
|
113
|
+
export function capBalances(balances, roles) {
|
|
114
|
+
const ordered = byRole(balances.map((b) => ({ ...b, role: roleOf(b.holder, roles) })));
|
|
115
|
+
return {
|
|
116
|
+
kept: ordered.slice(0, MAX_SIMULATED_BALANCES),
|
|
117
|
+
dropped: ordered.length > MAX_SIMULATED_BALANCES,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/** The changes to report: non-zero nets, each with its role and asset, the vault first. */
|
|
121
|
+
export function toChanges(balances, roles, token) {
|
|
122
|
+
return byRole(balances
|
|
123
|
+
.filter(({ delta }) => delta !== 0n)
|
|
124
|
+
.map(({ asset, holder, delta }) => ({
|
|
125
|
+
holder: getAddress(holder),
|
|
126
|
+
role: roleOf(holder, roles),
|
|
127
|
+
asset: asset === NATIVE_ASSET
|
|
128
|
+
? { kind: "native" }
|
|
129
|
+
: {
|
|
130
|
+
kind: "erc20",
|
|
131
|
+
address: getAddress(asset),
|
|
132
|
+
...(token(asset) ?? {
|
|
133
|
+
symbol: null,
|
|
134
|
+
decimals: null,
|
|
135
|
+
listed: false,
|
|
136
|
+
}),
|
|
137
|
+
},
|
|
138
|
+
delta: delta.toString(),
|
|
139
|
+
})));
|
|
140
|
+
}
|
|
141
|
+
/** The `balanceOf`, `symbol()` and `decimals()` reads a pass makes. */
|
|
142
|
+
export const tokenReads = {
|
|
143
|
+
balanceOf: (holder) => encodeFunctionData({
|
|
144
|
+
abi: ERC20,
|
|
145
|
+
functionName: "balanceOf",
|
|
146
|
+
args: [holder],
|
|
147
|
+
}),
|
|
148
|
+
symbol: encodeFunctionData({ abi: ERC20, functionName: "symbol" }),
|
|
149
|
+
decimals: encodeFunctionData({ abi: ERC20, functionName: "decimals" }),
|
|
150
|
+
};
|
|
151
|
+
/** `symbol()`'s result as the bytes a `string` is encoded as. */
|
|
152
|
+
const BYTES_RESULT = parseAbi(["function symbol() view returns (bytes)"]);
|
|
153
|
+
/** A token's own `symbol()`: a plain symbol, or null (a bytes32 one, NUL-padded, is read too). */
|
|
154
|
+
export function claimedSymbol(data) {
|
|
155
|
+
let symbol = null;
|
|
156
|
+
try {
|
|
157
|
+
// Read as bytes, so invalid UTF-8 is refused rather than replaced.
|
|
158
|
+
const raw = decodeFunctionResult({
|
|
159
|
+
abi: BYTES_RESULT,
|
|
160
|
+
functionName: "symbol",
|
|
161
|
+
data: data,
|
|
162
|
+
});
|
|
163
|
+
symbol = STRICT_UTF8.decode(hexToBytes(raw));
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
if (isWord(data)) {
|
|
167
|
+
const bytes = hexToBytes(data);
|
|
168
|
+
const end = bytes.indexOf(0);
|
|
169
|
+
try {
|
|
170
|
+
symbol = STRICT_UTF8.decode(end === -1 ? bytes : bytes.slice(0, end));
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
symbol = null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return symbol !== null && TokenSymbol.safeParse(symbol).success
|
|
178
|
+
? symbol
|
|
179
|
+
: null;
|
|
180
|
+
}
|
|
181
|
+
/** A token's own `decimals()`, or null when it is not one. */
|
|
182
|
+
export function claimedDecimals(data) {
|
|
183
|
+
try {
|
|
184
|
+
const decimals = decodeFunctionResult({
|
|
185
|
+
abi: ERC20,
|
|
186
|
+
functionName: "decimals",
|
|
187
|
+
data: data,
|
|
188
|
+
});
|
|
189
|
+
const parsed = TokenDecimals.safeParse(Number(decimals));
|
|
190
|
+
return parsed.success ? parsed.data : null;
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/** A balance read's value, or null when the read failed or is not a word. */
|
|
197
|
+
export function balanceOf(result) {
|
|
198
|
+
return BigInt(result.status) === 1n && isWord(result.returnData)
|
|
199
|
+
? BigInt(result.returnData)
|
|
200
|
+
: null;
|
|
201
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { type BalanceChange, type CallEffect, type CallSimulation } from "../types.ts";
|
|
2
|
+
import type { TokenInfo } from "./balances.ts";
|
|
3
|
+
/**
|
|
4
|
+
* How every surface shows a simulation or an executed effect (the web review,
|
|
5
|
+
* the signing email, the CLI): one description, rendered by each in its own
|
|
6
|
+
* layout with its own amount format.
|
|
7
|
+
*/
|
|
8
|
+
/** Every EVM chain Cofund runs on pays gas, and moves value, in ETH. */
|
|
9
|
+
export declare const NATIVE_SYMBOL = "ETH";
|
|
10
|
+
/** A token's name and decimals by address, null when unknown. */
|
|
11
|
+
export type TokenLookup = (address: string) => TokenInfo | null;
|
|
12
|
+
/** Base units as a decimal amount, in a surface's own format. */
|
|
13
|
+
export type AmountFormat = (units: bigint, decimals: number) => string;
|
|
14
|
+
/**
|
|
15
|
+
* The CLI's format: every digit, as ethers writes units. A whole amount keeps
|
|
16
|
+
* one decimal ("1.0"), trailing zeros go, a token without decimals has no
|
|
17
|
+
* point.
|
|
18
|
+
*/
|
|
19
|
+
export declare const formatUnitsAmount: AmountFormat;
|
|
20
|
+
/**
|
|
21
|
+
* An asset as approvers read it: the native coin and a token from Cofund's
|
|
22
|
+
* list by their symbol; any other token by its own claim, in quotes, with its
|
|
23
|
+
* address (a token's `symbol()` is anyone's to set).
|
|
24
|
+
*/
|
|
25
|
+
export declare function assetLabel(asset: BalanceChange["asset"]): string;
|
|
26
|
+
/** "Vault", "Wrapper", or the holder's address. */
|
|
27
|
+
export declare function holderLabel(change: Pick<BalanceChange, "holder" | "role">): string;
|
|
28
|
+
/** A change as a signed amount with its asset; none as "no change in" it. */
|
|
29
|
+
export declare function formatDelta(change: Pick<BalanceChange, "asset" | "delta">, format: AmountFormat, { withAddress }?: {
|
|
30
|
+
withAddress?: boolean;
|
|
31
|
+
}): string;
|
|
32
|
+
/** One holder and its changes. */
|
|
33
|
+
export type HolderEffect = {
|
|
34
|
+
holder: string;
|
|
35
|
+
role: BalanceChange["role"];
|
|
36
|
+
changes: BalanceChange[];
|
|
37
|
+
};
|
|
38
|
+
/** The changes per holder, the vault first, then the wrapper, then the others as they came. */
|
|
39
|
+
export declare function changesByHolder(changes: BalanceChange[]): HolderEffect[];
|
|
40
|
+
/** The vault's own changes in a successful simulation; none otherwise. */
|
|
41
|
+
export declare function vaultChanges(simulation: CallSimulation | null | undefined): BalanceChange[];
|
|
42
|
+
/** The tokens simulations named, by address: a listing over a token's claim, a claim with decimals over one without. */
|
|
43
|
+
export declare function simulationTokens(...simulations: (CallSimulation | CallEffect | null | undefined)[]): TokenLookup;
|
|
44
|
+
/**
|
|
45
|
+
* The hard bound every surface ends with: the vault approves exactly the
|
|
46
|
+
* signed allowances to the wrapper for this call and resets them, so per
|
|
47
|
+
* asset it can lose at most the allowance (the last one listed when an asset
|
|
48
|
+
* repeats). Null allowances (stored ones that do not parse) leave it unknown.
|
|
49
|
+
*/
|
|
50
|
+
export declare function allowanceBound(allowances: {
|
|
51
|
+
asset: string;
|
|
52
|
+
amount: string;
|
|
53
|
+
}[] | null, tokens: TokenLookup, format: AmountFormat): string;
|
|
54
|
+
/** What every surface says after the effect: the caveat, and on an unlisted wrapper its warning. */
|
|
55
|
+
export declare function simulationCaveat(unlisted: boolean): string[];
|
|
56
|
+
/** "2026-10-15 14:00 UTC". */
|
|
57
|
+
export declare function utcDateTime(iso: string): string;
|
|
58
|
+
/** A simulation or an executed effect as every surface lays it out. */
|
|
59
|
+
export type SimulationView = {
|
|
60
|
+
status: "success" | "revert" | "unavailable" | "executed";
|
|
61
|
+
/** The block it ran on top of (or landed in); null when unavailable. */
|
|
62
|
+
block: string | null;
|
|
63
|
+
at: string;
|
|
64
|
+
/** Each holder's changes, the vault first. */
|
|
65
|
+
holders: {
|
|
66
|
+
label: string;
|
|
67
|
+
address: string;
|
|
68
|
+
role: BalanceChange["role"];
|
|
69
|
+
changes: string[];
|
|
70
|
+
}[];
|
|
71
|
+
/** Why it would fail, why it could not be simulated, or that nothing moves. */
|
|
72
|
+
notice: string | null;
|
|
73
|
+
/** What it does not show. */
|
|
74
|
+
notes: string[];
|
|
75
|
+
};
|
|
76
|
+
/** `simulation` (or what an executed call did) described, amounts in `format`. */
|
|
77
|
+
export declare function describeSimulation(simulation: CallSimulation | CallEffect, format: AmountFormat, { withAddress }?: {
|
|
78
|
+
withAddress?: boolean;
|
|
79
|
+
}): SimulationView;
|