maroo-viem-poc 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/README.md +146 -0
- package/dist/index.cjs +740 -0
- package/dist/index.d.cts +3637 -0
- package/dist/index.d.mts +3637 -0
- package/dist/index.mjs +717 -0
- package/package.json +59 -0
- package/src/actions/runOnPcl.ts +117 -0
- package/src/codec/policy.ts +381 -0
- package/src/constants.ts +57 -0
- package/src/core/precompileActions.ts +215 -0
- package/src/decorators/maroo.ts +114 -0
- package/src/decorators/precompiles.ts +44 -0
- package/src/errors/pcl.ts +137 -0
- package/src/index.ts +58 -0
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "maroo-viem-poc",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "PoC viem extension for the Maroo chain — namespaced precompile actions (pcl/okrw/eas/agent), PCL policy codec, runOnPcl wrapper, and typed policy-violation decoding. Verified against live marooTestnet.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=20"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"maroo",
|
|
13
|
+
"viem",
|
|
14
|
+
"precompile",
|
|
15
|
+
"pcl",
|
|
16
|
+
"cosmos-evm",
|
|
17
|
+
"poc"
|
|
18
|
+
],
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"src",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"import": {
|
|
27
|
+
"types": "./dist/index.d.mts",
|
|
28
|
+
"default": "./dist/index.mjs"
|
|
29
|
+
},
|
|
30
|
+
"require": {
|
|
31
|
+
"types": "./dist/index.d.cts",
|
|
32
|
+
"default": "./dist/index.cjs"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"main": "./dist/index.cjs",
|
|
37
|
+
"module": "./dist/index.mjs",
|
|
38
|
+
"types": "./dist/index.d.cts",
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@maroo-chain/contracts": "0.0.6",
|
|
41
|
+
"abitype": "^1.0.0"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"viem": "^2.44.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^26.1.1",
|
|
48
|
+
"tsdown": "^0.21.0",
|
|
49
|
+
"typescript": "^5.7.0",
|
|
50
|
+
"viem": "^2.44.0",
|
|
51
|
+
"vitest": "^2.1.9"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "tsdown",
|
|
55
|
+
"typecheck": "tsc --noEmit",
|
|
56
|
+
"test": "vitest run test/unit",
|
|
57
|
+
"test:live": "vitest run test/live"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { iPclAbi } from '@maroo-chain/contracts/abi/precompiles/pcl/IPcl'
|
|
2
|
+
import {
|
|
3
|
+
type Abi,
|
|
4
|
+
type Account,
|
|
5
|
+
type Address,
|
|
6
|
+
type Chain,
|
|
7
|
+
type Client,
|
|
8
|
+
type ContractFunctionArgs,
|
|
9
|
+
type ContractFunctionName,
|
|
10
|
+
type Hex,
|
|
11
|
+
type Transport,
|
|
12
|
+
type WriteContractReturnType,
|
|
13
|
+
encodeFunctionData,
|
|
14
|
+
} from 'viem'
|
|
15
|
+
import { writeContract } from 'viem/actions'
|
|
16
|
+
import { getAction } from 'viem/utils'
|
|
17
|
+
import { precompileAddresses } from '../constants.js'
|
|
18
|
+
|
|
19
|
+
type RunOnPclBase = {
|
|
20
|
+
/** The contract — or plain account — to call *through* PCL. */
|
|
21
|
+
address: Address
|
|
22
|
+
/** Native value forwarded to `address`. Debited from the caller, not from `msg.value`. */
|
|
23
|
+
value?: bigint | undefined
|
|
24
|
+
gas?: bigint | undefined
|
|
25
|
+
/** Override the PCL precompile address. */
|
|
26
|
+
pclAddress?: Address | undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type RunOnPclContractCall<
|
|
30
|
+
abi extends Abi | readonly unknown[],
|
|
31
|
+
functionName extends ContractFunctionName<abi, 'nonpayable' | 'payable'>,
|
|
32
|
+
> = RunOnPclBase & {
|
|
33
|
+
abi: abi
|
|
34
|
+
functionName: functionName
|
|
35
|
+
args?: ContractFunctionArgs<abi, 'nonpayable' | 'payable', functionName>
|
|
36
|
+
data?: undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A bare value transfer, or a call whose calldata you already encoded. */
|
|
40
|
+
type RunOnPclRawCall = RunOnPclBase & {
|
|
41
|
+
abi?: undefined
|
|
42
|
+
functionName?: undefined
|
|
43
|
+
args?: undefined
|
|
44
|
+
data?: Hex | undefined
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type RunOnPclParameters<
|
|
48
|
+
abi extends Abi | readonly unknown[] = Abi,
|
|
49
|
+
functionName extends ContractFunctionName<abi, 'nonpayable' | 'payable'> = ContractFunctionName<
|
|
50
|
+
abi,
|
|
51
|
+
'nonpayable' | 'payable'
|
|
52
|
+
>,
|
|
53
|
+
> = RunOnPclContractCall<abi, functionName> | RunOnPclRawCall
|
|
54
|
+
|
|
55
|
+
export type RunOnPclReturnType = WriteContractReturnType
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Call a contract *through* the PCL precompile so that contract-scoped
|
|
59
|
+
* policies are enforced.
|
|
60
|
+
*
|
|
61
|
+
* A direct call to the same contract enforces **global policies only**. There
|
|
62
|
+
* is nothing in the target contract's ABI, nor in any revert, that signals
|
|
63
|
+
* this — the call simply succeeds with contract policies unapplied. Route
|
|
64
|
+
* through `runOnPcl` whenever the target has policies registered.
|
|
65
|
+
*
|
|
66
|
+
* For a contract call the parameter shape mirrors `writeContract`, so
|
|
67
|
+
* switching a call site is a one-word change. Omit `abi`/`functionName` to
|
|
68
|
+
* forward a bare native transfer, or pass pre-encoded `data`.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```ts
|
|
72
|
+
* const hash = await client.pcl.runOnPcl({
|
|
73
|
+
* address: token,
|
|
74
|
+
* abi: erc20Abi,
|
|
75
|
+
* functionName: 'transfer',
|
|
76
|
+
* args: [recipient, amount],
|
|
77
|
+
* })
|
|
78
|
+
*
|
|
79
|
+
* // Native transfer, policy-checked:
|
|
80
|
+
* const hash = await client.pcl.runOnPcl({ address: recipient, value: parseEther('1') })
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export async function runOnPcl<
|
|
84
|
+
chain extends Chain | undefined,
|
|
85
|
+
account extends Account | undefined,
|
|
86
|
+
const abi extends Abi | readonly unknown[],
|
|
87
|
+
functionName extends ContractFunctionName<abi, 'nonpayable' | 'payable'>,
|
|
88
|
+
>(
|
|
89
|
+
client: Client<Transport, chain, account>,
|
|
90
|
+
parameters: RunOnPclParameters<abi, functionName>,
|
|
91
|
+
): Promise<RunOnPclReturnType> {
|
|
92
|
+
const { address, value = 0n, gas, pclAddress } = parameters
|
|
93
|
+
|
|
94
|
+
const data: Hex = parameters.abi
|
|
95
|
+
? (encodeFunctionData({
|
|
96
|
+
abi: parameters.abi,
|
|
97
|
+
functionName: parameters.functionName,
|
|
98
|
+
args: parameters.args,
|
|
99
|
+
} as never) as Hex)
|
|
100
|
+
: (parameters.data ?? '0x')
|
|
101
|
+
|
|
102
|
+
return getAction(
|
|
103
|
+
client,
|
|
104
|
+
writeContract,
|
|
105
|
+
'writeContract',
|
|
106
|
+
)({
|
|
107
|
+
address: pclAddress ?? precompileAddresses.pcl,
|
|
108
|
+
abi: iPclAbi,
|
|
109
|
+
functionName: 'runOnPcl',
|
|
110
|
+
args: [address, data, value],
|
|
111
|
+
// `value` rides on the argument, never on the outer tx. The precompile
|
|
112
|
+
// issues `evm.Call(caller, target, data, gas, value)`, which debits the
|
|
113
|
+
// original caller directly. Attaching `msg.value` here would strand funds
|
|
114
|
+
// at the precompile.
|
|
115
|
+
gas,
|
|
116
|
+
} as never)
|
|
117
|
+
}
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import type { AbiParametersToPrimitiveTypes } from 'abitype'
|
|
2
|
+
import { type Hex, decodeAbiParameters, encodeAbiParameters } from 'viem'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* PCL policy codec.
|
|
6
|
+
*
|
|
7
|
+
* `PolicySet.policy` is an opaque `bytes` field whose layout is chosen by the
|
|
8
|
+
* sibling `templateId` string. That mapping exists nowhere in the ABI — the
|
|
9
|
+
* `_policies` dummy function only forces the structs into the artifact. This
|
|
10
|
+
* module is the mapping.
|
|
11
|
+
*
|
|
12
|
+
* Verified against `marooTestnet.globalPolicies()` at block 12447862.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// ─── Template ids ────────────────────────────────────────────────────────────
|
|
16
|
+
// Confirmed live: every id below returns a registered template from
|
|
17
|
+
// `pcl.policyTemplate(id)` on marooTestnet. Note FOR_EACH_POLICY, not
|
|
18
|
+
// FOREACH_POLICY — the underscore is load-bearing.
|
|
19
|
+
|
|
20
|
+
export const policyTemplateIds = [
|
|
21
|
+
'EAS_POLICY',
|
|
22
|
+
'DENYLIST_POLICY',
|
|
23
|
+
'VOLUME_POLICY',
|
|
24
|
+
'OKRW_EAS_TRANSFER_LIMIT_POLICY',
|
|
25
|
+
'PERIODIC_VOLUME_POLICY',
|
|
26
|
+
'AGENT_OKRW_TRANSFER_LIMIT_POLICY',
|
|
27
|
+
'OKRW_EAS_PERIODIC_VOLUME_LIMIT_POLICY',
|
|
28
|
+
'LOGICAL_POLICY',
|
|
29
|
+
'FOR_EACH_POLICY',
|
|
30
|
+
] as const
|
|
31
|
+
|
|
32
|
+
export type PolicyTemplateId = (typeof policyTemplateIds)[number]
|
|
33
|
+
|
|
34
|
+
// ─── Enums (IPcl.sol) ────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
export const LogicalQuantifier = { Unspecified: 0, And: 1, Or: 2 } as const
|
|
37
|
+
export const ForEachQuantifier = { Unspecified: 0, Any: 1, Every: 2 } as const
|
|
38
|
+
export const ForEachSubject = { Unspecified: 0, AgentOwners: 1 } as const
|
|
39
|
+
|
|
40
|
+
export type LogicalQuantifierName = keyof typeof LogicalQuantifier
|
|
41
|
+
export type ForEachQuantifierName = keyof typeof ForEachQuantifier
|
|
42
|
+
export type ForEachSubjectName = keyof typeof ForEachSubject
|
|
43
|
+
|
|
44
|
+
const nameOf = <T extends Record<string, number>>(e: T, v: number): keyof T =>
|
|
45
|
+
(Object.keys(e) as (keyof T)[]).find((k) => e[k] === v) ?? ('Unspecified' as keyof T)
|
|
46
|
+
|
|
47
|
+
// ─── ABI parameter shapes ────────────────────────────────────────────────────
|
|
48
|
+
//
|
|
49
|
+
// Plain `as const` literals — no helper. A helper that takes `(name: string,
|
|
50
|
+
// type: string)` widens the literals before `as const` can pin them, which
|
|
51
|
+
// erases the type `decodeAbiParameters` needs to infer a tuple. Written out,
|
|
52
|
+
// abitype infers each payload's shape and the decode path below is cast-free.
|
|
53
|
+
// (`parseAbiParameters` is not an option: it rejects the nested tuples that
|
|
54
|
+
// LOGICAL_POLICY and FOR_EACH_POLICY require.)
|
|
55
|
+
|
|
56
|
+
const POLICY_SET_COMPONENTS = [
|
|
57
|
+
{ name: 'templateId', type: 'string' },
|
|
58
|
+
{ name: 'policy', type: 'bytes' },
|
|
59
|
+
{ name: 'selector', type: 'bytes' },
|
|
60
|
+
] as const
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* `templateId` → the ABI parameters of its `policy` bytes payload.
|
|
64
|
+
*
|
|
65
|
+
* Each entry is a one-element parameter list wrapping the payload tuple, so
|
|
66
|
+
* `decodeAbiParameters(codec, bytes)[0]` is the decoded struct.
|
|
67
|
+
*/
|
|
68
|
+
export const policyCodecs = {
|
|
69
|
+
EAS_POLICY: [
|
|
70
|
+
{
|
|
71
|
+
name: 'policy',
|
|
72
|
+
type: 'tuple',
|
|
73
|
+
components: [
|
|
74
|
+
{ name: 'easContract', type: 'address' },
|
|
75
|
+
{ name: 'indexContract', type: 'address' },
|
|
76
|
+
{ name: 'schemaUid', type: 'bytes32' },
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
DENYLIST_POLICY: [
|
|
81
|
+
{ name: 'policy', type: 'tuple', components: [{ name: 'addresses', type: 'address[]' }] },
|
|
82
|
+
],
|
|
83
|
+
VOLUME_POLICY: [
|
|
84
|
+
{
|
|
85
|
+
name: 'policy',
|
|
86
|
+
type: 'tuple',
|
|
87
|
+
components: [
|
|
88
|
+
{ name: 'tokens', type: 'string[]' },
|
|
89
|
+
{
|
|
90
|
+
name: 'limits',
|
|
91
|
+
type: 'tuple[]',
|
|
92
|
+
components: [
|
|
93
|
+
{ name: 'minLimit', type: 'uint256' },
|
|
94
|
+
{ name: 'maxLimit', type: 'uint256' },
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
PERIODIC_VOLUME_POLICY: [
|
|
101
|
+
{
|
|
102
|
+
name: 'policy',
|
|
103
|
+
type: 'tuple',
|
|
104
|
+
components: [
|
|
105
|
+
{ name: 'tokens', type: 'string[]' },
|
|
106
|
+
{
|
|
107
|
+
name: 'limits',
|
|
108
|
+
type: 'tuple[]',
|
|
109
|
+
components: [
|
|
110
|
+
{ name: 'maxAmount', type: 'uint256' },
|
|
111
|
+
{ name: 'resetPeriodSeconds', type: 'uint64' },
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
OKRW_EAS_TRANSFER_LIMIT_POLICY: [
|
|
118
|
+
{
|
|
119
|
+
name: 'policy',
|
|
120
|
+
type: 'tuple',
|
|
121
|
+
components: [
|
|
122
|
+
{ name: 'easContract', type: 'address' },
|
|
123
|
+
{ name: 'indexContract', type: 'address' },
|
|
124
|
+
{ name: 'schemaUid', type: 'bytes32' },
|
|
125
|
+
{ name: 'transferLimitAmount', type: 'uint256' },
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
OKRW_EAS_PERIODIC_VOLUME_LIMIT_POLICY: [
|
|
130
|
+
{
|
|
131
|
+
name: 'policy',
|
|
132
|
+
type: 'tuple',
|
|
133
|
+
components: [
|
|
134
|
+
{ name: 'easContract', type: 'address' },
|
|
135
|
+
{ name: 'indexContract', type: 'address' },
|
|
136
|
+
{ name: 'schemaUid', type: 'bytes32' },
|
|
137
|
+
{ name: 'maxAmount', type: 'uint256' },
|
|
138
|
+
{ name: 'resetPeriodSeconds', type: 'uint64' },
|
|
139
|
+
],
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
AGENT_OKRW_TRANSFER_LIMIT_POLICY: [
|
|
143
|
+
{ name: 'policy', type: 'tuple', components: [{ name: 'reserved', type: 'uint256' }] },
|
|
144
|
+
],
|
|
145
|
+
LOGICAL_POLICY: [
|
|
146
|
+
{
|
|
147
|
+
name: 'policy',
|
|
148
|
+
type: 'tuple',
|
|
149
|
+
components: [
|
|
150
|
+
{ name: 'quantifier', type: 'uint8' },
|
|
151
|
+
{ name: 'children', type: 'tuple[]', components: POLICY_SET_COMPONENTS },
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
FOR_EACH_POLICY: [
|
|
156
|
+
{
|
|
157
|
+
name: 'policy',
|
|
158
|
+
type: 'tuple',
|
|
159
|
+
components: [
|
|
160
|
+
{ name: 'quantifier', type: 'uint8' },
|
|
161
|
+
{ name: 'subject', type: 'uint8' },
|
|
162
|
+
{ name: 'child', type: 'tuple', components: POLICY_SET_COMPONENTS },
|
|
163
|
+
],
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
} as const
|
|
167
|
+
|
|
168
|
+
/** The input struct expected by {@link encodePolicy} for a given template. */
|
|
169
|
+
export type PolicyInput<id extends PolicyTemplateId> = AbiParametersToPrimitiveTypes<
|
|
170
|
+
(typeof policyCodecs)[id]
|
|
171
|
+
>[0]
|
|
172
|
+
|
|
173
|
+
// ─── Wire type ───────────────────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
/** A `PolicySet` exactly as the precompile returns it. */
|
|
176
|
+
export type RawPolicySet = {
|
|
177
|
+
templateId: string
|
|
178
|
+
policy: Hex
|
|
179
|
+
selector: Hex
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ─── Decoded tree ────────────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
export type DecodedPolicy =
|
|
185
|
+
| { templateId: 'EAS_POLICY'; selector: Hex; easContract: Hex; indexContract: Hex; schemaUid: Hex }
|
|
186
|
+
| { templateId: 'DENYLIST_POLICY'; selector: Hex; addresses: readonly Hex[] }
|
|
187
|
+
| {
|
|
188
|
+
templateId: 'VOLUME_POLICY'
|
|
189
|
+
selector: Hex
|
|
190
|
+
limits: readonly { token: string; minLimit: bigint; maxLimit: bigint }[]
|
|
191
|
+
}
|
|
192
|
+
| {
|
|
193
|
+
templateId: 'PERIODIC_VOLUME_POLICY'
|
|
194
|
+
selector: Hex
|
|
195
|
+
limits: readonly { token: string; maxAmount: bigint; resetPeriodSeconds: bigint }[]
|
|
196
|
+
}
|
|
197
|
+
| {
|
|
198
|
+
templateId: 'OKRW_EAS_TRANSFER_LIMIT_POLICY'
|
|
199
|
+
selector: Hex
|
|
200
|
+
easContract: Hex
|
|
201
|
+
indexContract: Hex
|
|
202
|
+
schemaUid: Hex
|
|
203
|
+
transferLimitAmount: bigint
|
|
204
|
+
}
|
|
205
|
+
| {
|
|
206
|
+
templateId: 'OKRW_EAS_PERIODIC_VOLUME_LIMIT_POLICY'
|
|
207
|
+
selector: Hex
|
|
208
|
+
easContract: Hex
|
|
209
|
+
indexContract: Hex
|
|
210
|
+
schemaUid: Hex
|
|
211
|
+
maxAmount: bigint
|
|
212
|
+
resetPeriodSeconds: bigint
|
|
213
|
+
}
|
|
214
|
+
/** The `reserved` field is ignored by the chain, so it is not surfaced. */
|
|
215
|
+
| { templateId: 'AGENT_OKRW_TRANSFER_LIMIT_POLICY'; selector: Hex }
|
|
216
|
+
| { templateId: 'LOGICAL_POLICY'; selector: Hex; quantifier: LogicalQuantifierName; children: DecodedPolicy[] }
|
|
217
|
+
| {
|
|
218
|
+
templateId: 'FOR_EACH_POLICY'
|
|
219
|
+
selector: Hex
|
|
220
|
+
quantifier: ForEachQuantifierName
|
|
221
|
+
subject: ForEachSubjectName
|
|
222
|
+
child: DecodedPolicy
|
|
223
|
+
}
|
|
224
|
+
/** Any template this SDK version does not model yet. Never throws. */
|
|
225
|
+
| { templateId: string; selector: Hex; raw: Hex; unknown: true }
|
|
226
|
+
|
|
227
|
+
const MAX_DEPTH = 8
|
|
228
|
+
|
|
229
|
+
const isKnown = (id: string): id is PolicyTemplateId =>
|
|
230
|
+
Object.prototype.hasOwnProperty.call(policyCodecs, id)
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Recursively decode a `PolicySet` returned by `globalPolicies()` or
|
|
234
|
+
* `contractPolicies()` into a readable tree.
|
|
235
|
+
*
|
|
236
|
+
* Unrecognised template ids decode to `{ unknown: true, raw }` rather than
|
|
237
|
+
* throwing, so a chain upgrade that adds a policy type degrades a dApp's
|
|
238
|
+
* policy inspector instead of breaking it.
|
|
239
|
+
*/
|
|
240
|
+
export function decodePolicySet(set: RawPolicySet, depth = 0): DecodedPolicy {
|
|
241
|
+
if (depth > MAX_DEPTH) throw new Error(`policy nesting exceeded depth ${MAX_DEPTH}`)
|
|
242
|
+
|
|
243
|
+
const { selector } = set
|
|
244
|
+
if (!isKnown(set.templateId))
|
|
245
|
+
return { templateId: set.templateId, selector, raw: set.policy, unknown: true }
|
|
246
|
+
|
|
247
|
+
// Each branch indexes its codec by literal key, so `decodeAbiParameters`
|
|
248
|
+
// infers the exact payload tuple — no casts.
|
|
249
|
+
switch (set.templateId) {
|
|
250
|
+
case 'EAS_POLICY': {
|
|
251
|
+
const [p] = decodeAbiParameters(policyCodecs.EAS_POLICY, set.policy)
|
|
252
|
+
return {
|
|
253
|
+
templateId: 'EAS_POLICY',
|
|
254
|
+
selector,
|
|
255
|
+
easContract: p.easContract,
|
|
256
|
+
indexContract: p.indexContract,
|
|
257
|
+
schemaUid: p.schemaUid,
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
case 'DENYLIST_POLICY': {
|
|
261
|
+
const [p] = decodeAbiParameters(policyCodecs.DENYLIST_POLICY, set.policy)
|
|
262
|
+
return { templateId: 'DENYLIST_POLICY', selector, addresses: p.addresses }
|
|
263
|
+
}
|
|
264
|
+
case 'VOLUME_POLICY': {
|
|
265
|
+
const [p] = decodeAbiParameters(policyCodecs.VOLUME_POLICY, set.policy)
|
|
266
|
+
// tokens[i] pairs with limits[i], but the ABI does not tie the array
|
|
267
|
+
// lengths — a mis-encoded policy must degrade to raw, not throw.
|
|
268
|
+
if (p.tokens.length !== p.limits.length)
|
|
269
|
+
return { templateId: set.templateId, selector, raw: set.policy, unknown: true }
|
|
270
|
+
return {
|
|
271
|
+
templateId: 'VOLUME_POLICY',
|
|
272
|
+
selector,
|
|
273
|
+
limits: p.tokens.map((token, i) => ({
|
|
274
|
+
token,
|
|
275
|
+
minLimit: p.limits[i].minLimit,
|
|
276
|
+
maxLimit: p.limits[i].maxLimit,
|
|
277
|
+
})),
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
case 'PERIODIC_VOLUME_POLICY': {
|
|
281
|
+
const [p] = decodeAbiParameters(policyCodecs.PERIODIC_VOLUME_POLICY, set.policy)
|
|
282
|
+
if (p.tokens.length !== p.limits.length)
|
|
283
|
+
return { templateId: set.templateId, selector, raw: set.policy, unknown: true }
|
|
284
|
+
return {
|
|
285
|
+
templateId: 'PERIODIC_VOLUME_POLICY',
|
|
286
|
+
selector,
|
|
287
|
+
limits: p.tokens.map((token, i) => ({
|
|
288
|
+
token,
|
|
289
|
+
maxAmount: p.limits[i].maxAmount,
|
|
290
|
+
resetPeriodSeconds: p.limits[i].resetPeriodSeconds,
|
|
291
|
+
})),
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
case 'OKRW_EAS_TRANSFER_LIMIT_POLICY': {
|
|
295
|
+
const [p] = decodeAbiParameters(policyCodecs.OKRW_EAS_TRANSFER_LIMIT_POLICY, set.policy)
|
|
296
|
+
return {
|
|
297
|
+
templateId: 'OKRW_EAS_TRANSFER_LIMIT_POLICY',
|
|
298
|
+
selector,
|
|
299
|
+
easContract: p.easContract,
|
|
300
|
+
indexContract: p.indexContract,
|
|
301
|
+
schemaUid: p.schemaUid,
|
|
302
|
+
transferLimitAmount: p.transferLimitAmount,
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
case 'OKRW_EAS_PERIODIC_VOLUME_LIMIT_POLICY': {
|
|
306
|
+
const [p] = decodeAbiParameters(policyCodecs.OKRW_EAS_PERIODIC_VOLUME_LIMIT_POLICY, set.policy)
|
|
307
|
+
return {
|
|
308
|
+
templateId: 'OKRW_EAS_PERIODIC_VOLUME_LIMIT_POLICY',
|
|
309
|
+
selector,
|
|
310
|
+
easContract: p.easContract,
|
|
311
|
+
indexContract: p.indexContract,
|
|
312
|
+
schemaUid: p.schemaUid,
|
|
313
|
+
maxAmount: p.maxAmount,
|
|
314
|
+
resetPeriodSeconds: p.resetPeriodSeconds,
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
case 'AGENT_OKRW_TRANSFER_LIMIT_POLICY': {
|
|
318
|
+
// Decode to validate the payload layout; `reserved` itself is ignored
|
|
319
|
+
// by the chain and not surfaced.
|
|
320
|
+
decodeAbiParameters(policyCodecs.AGENT_OKRW_TRANSFER_LIMIT_POLICY, set.policy)
|
|
321
|
+
return { templateId: 'AGENT_OKRW_TRANSFER_LIMIT_POLICY', selector }
|
|
322
|
+
}
|
|
323
|
+
case 'LOGICAL_POLICY': {
|
|
324
|
+
const [p] = decodeAbiParameters(policyCodecs.LOGICAL_POLICY, set.policy)
|
|
325
|
+
return {
|
|
326
|
+
templateId: 'LOGICAL_POLICY',
|
|
327
|
+
selector,
|
|
328
|
+
quantifier: nameOf(LogicalQuantifier, p.quantifier),
|
|
329
|
+
children: p.children.map((c) => decodePolicySet(c, depth + 1)),
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
case 'FOR_EACH_POLICY': {
|
|
333
|
+
const [p] = decodeAbiParameters(policyCodecs.FOR_EACH_POLICY, set.policy)
|
|
334
|
+
return {
|
|
335
|
+
templateId: 'FOR_EACH_POLICY',
|
|
336
|
+
selector,
|
|
337
|
+
quantifier: nameOf(ForEachQuantifier, p.quantifier),
|
|
338
|
+
subject: nameOf(ForEachSubject, p.subject),
|
|
339
|
+
child: decodePolicySet(p.child, depth + 1),
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
// Unreachable while the switch covers every id in `policyCodecs`; kept so
|
|
343
|
+
// a codec added without a decode branch degrades instead of crashing.
|
|
344
|
+
default:
|
|
345
|
+
return { templateId: set.templateId, selector, raw: set.policy, unknown: true }
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* ABI-encode a policy payload for its template.
|
|
351
|
+
*
|
|
352
|
+
* The `policy` argument is typed per `templateId` via {@link PolicyInput}, so
|
|
353
|
+
* the public signature is fully checked. The single internal cast is the
|
|
354
|
+
* unavoidable one: indexing `policyCodecs` by a generic `id` yields a union
|
|
355
|
+
* that `encodeAbiParameters` cannot unify positionally (viem does the same at
|
|
356
|
+
* this boundary).
|
|
357
|
+
*
|
|
358
|
+
* Admin-facing: only `policyAdmin` may set global policies, and only a
|
|
359
|
+
* contract's registered admin may change its contract policies.
|
|
360
|
+
*/
|
|
361
|
+
export function encodePolicy<id extends PolicyTemplateId>(templateId: id, policy: PolicyInput<id>): Hex {
|
|
362
|
+
const codec = policyCodecs[templateId]
|
|
363
|
+
return encodeAbiParameters(codec, [policy] as never)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Normalize a function selector for a `PolicySet`.
|
|
368
|
+
*
|
|
369
|
+
* The chain accepts either an empty value (wildcard, stored internally as
|
|
370
|
+
* `"__all__"`) or exactly 4 bytes, lowercased. Anything else is rejected with
|
|
371
|
+
* `InvalidSelector`.
|
|
372
|
+
*/
|
|
373
|
+
export function normalizeSelector(selector?: Hex | undefined): Hex {
|
|
374
|
+
if (!selector || selector === '0x') return '0x'
|
|
375
|
+
const body = selector.slice(2).toLowerCase()
|
|
376
|
+
// Length alone is not enough: a non-hex body would pass here and only be
|
|
377
|
+
// rejected chain-side with an opaque InvalidSelector after gas was spent.
|
|
378
|
+
if (!/^[0-9a-f]{8}$/.test(body))
|
|
379
|
+
throw new Error(`selector must be 4 bytes of hex (8 hex chars), got "${selector}"`)
|
|
380
|
+
return `0x${body}`
|
|
381
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type Address, maxUint256 } from 'viem'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Maroo static precompile addresses.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors the `*_PRECOMPILED_ADDRESS` constants in
|
|
7
|
+
* `@maroo-chain/contracts/precompiles/{okrw,pcl,eas,agent}/I*.sol`.
|
|
8
|
+
*
|
|
9
|
+
* The contracts package does not export these from its TypeScript build
|
|
10
|
+
* (only the `.sol` sources carry them), so we redeclare them here.
|
|
11
|
+
* `test/addresses.test.ts` asserts these still match the installed package.
|
|
12
|
+
*/
|
|
13
|
+
export const precompileAddresses = {
|
|
14
|
+
okrw: '0x1000000000000000000000000000000000000001',
|
|
15
|
+
pcl: '0x1000000000000000000000000000000000000005',
|
|
16
|
+
/** Returns `eas` module params. Not the EAS contract itself — see {@link easContracts}. */
|
|
17
|
+
eas: '0x1000000000000000000000000000000000000009',
|
|
18
|
+
agent: '0x100000000000000000000000000000000000000A',
|
|
19
|
+
/** Clairveil shielded pool. Not yet deployed on marooTestnet. */
|
|
20
|
+
privacy: '0x100000000000000000000000000000000000000b',
|
|
21
|
+
} as const satisfies Record<string, Address>
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* EAS contracts, as reported by `eas.getParams()` on marooTestnet.
|
|
25
|
+
*
|
|
26
|
+
* These sit at precompile-shaped addresses but implement the standard
|
|
27
|
+
* Ethereum Attestation Service interfaces. `@maroo-chain/contracts@0.0.6`
|
|
28
|
+
* ships no ABI for them; use `@ethereum-attestation-service/eas-contracts`.
|
|
29
|
+
*
|
|
30
|
+
* Do not hardcode these in application code — read them from
|
|
31
|
+
* `client.eas.getParams()`, which is authoritative per network.
|
|
32
|
+
*/
|
|
33
|
+
export const easContracts = {
|
|
34
|
+
schemaRegistry: '0x1000000000000000000000000000000000000006',
|
|
35
|
+
eas: '0x1000000000000000000000000000000000000007',
|
|
36
|
+
indexer: '0x1000000000000000000000000000000000000008',
|
|
37
|
+
} as const satisfies Record<string, Address>
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* ABI entries that exist for chain-side codegen or proxy plumbing and must
|
|
41
|
+
* never be surfaced as client actions.
|
|
42
|
+
*
|
|
43
|
+
* - `_policies` is a `pure` no-op declared solely so the Solidity compiler
|
|
44
|
+
* emits the policy structs into the ABI (see the comment at the bottom of
|
|
45
|
+
* `IPcl.sol`). Calling it does nothing.
|
|
46
|
+
* - `preCall`/`postCall` are invoked by the PCL proxy hook with a `principal`
|
|
47
|
+
* the caller cannot forge. Application code goes through {@link runOnPcl}.
|
|
48
|
+
*/
|
|
49
|
+
export const excludedFunctions = {
|
|
50
|
+
pcl: ['_policies', 'preCall', 'postCall'],
|
|
51
|
+
} as const
|
|
52
|
+
|
|
53
|
+
/** The selector wildcard. An empty `selector` matches every function. */
|
|
54
|
+
export const SELECTOR_ALL = '0x' as const
|
|
55
|
+
|
|
56
|
+
/** `VolumeUnitPolicy.maxLimit` sentinel meaning "no upper bound". */
|
|
57
|
+
export const NO_MAX_LIMIT = maxUint256
|