Skip to content

Commit f059043

Browse files
authored
Add makePermissionDecoderConfigs to resolve the PermissionDecoderConfigs to be used with @metamask/gator-permissions-controller (#259)
* First pass at permission decoders * Shuffle permission decoding logic * Switch to use terms decoders from @metamask/delegation-core * Remove makePermissionDecoder and update tests to correctly verify configuration objects * Remove more cruft * Add changelog * All permission decoders are now permission-type specific typed, ensuring returned data is appropriately typed. * Use hexToNumber to parse startTime in erc20TokenAllowance decoder * Construct valid terms directly in tests, rather than depending on @metamask/delegation-core encoders * Rename expiryRule to expiryRuleDecoder Update payeeRuleDecoder to use getTermsByEnforcer utility * Refactor getTermsByEnforcer for readability * Tidy up redeemerDecoder * Normalize tests a little bit * Move @metamask/delegation-deployments to devDependencies * splitHex now returns a correctly sized Hex[] array * Export DeployedContractsByName and PermissionDecoderConfig types
1 parent 6a76335 commit f059043

33 files changed

Lines changed: 3080 additions & 4 deletions

packages/7715-permission-types/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Add `makePermissionDecoderConfigs` to resolve the `PermissionDecoderConfig`s to be used with @metamask/gator-permissions-controller ([#259](https://github.com/MetaMask/smart-accounts-kit/pull/259))
13+
1014
## [0.7.1]
1115

1216
### Fixed
Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,24 @@
1-
// eslint-disable-next-line
2-
export { default } from '../../shared/config/base.eslint.mjs';
1+
// eslint-disable-next-line import-x/extensions
2+
import baseConfig from '../../shared/config/base.eslint.mjs';
3+
4+
const config = [
5+
...baseConfig,
6+
{
7+
files: ['test/**/*.ts'],
8+
languageOptions: {
9+
parserOptions: {
10+
projectService: false,
11+
project: ['./tsconfig.eslint.json'],
12+
},
13+
},
14+
settings: {
15+
'import/resolver': {
16+
typescript: {
17+
project: './tsconfig.eslint.json',
18+
},
19+
},
20+
},
21+
},
22+
];
23+
24+
export default config;

packages/7715-permission-types/package.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
"scripts": {
4444
"build": "yarn typecheck && tsup",
4545
"typecheck": "tsc --noEmit",
46+
"test": "vitest run",
47+
"test:watch": "vitest",
4648
"lint": "yarn lint:eslint",
4749
"lint:eslint": "eslint . --cache --ext js,ts",
4850
"lint:fix": "yarn lint:eslint --fix",
@@ -55,10 +57,17 @@
5557
},
5658
"devDependencies": {
5759
"@metamask/auto-changelog": "^6.1.1",
60+
"@metamask/delegation-deployments": "^1.4.0",
61+
"@types/node": "^20.19.0",
5862
"compare-versions": "^6.1.1",
5963
"eslint": "^9.39.2",
6064
"prettier": "^3.5.3",
6165
"tsup": "^8.5.0",
62-
"typescript": "5.5.4"
66+
"typescript": "5.5.4",
67+
"vitest": "^3.2.4"
68+
},
69+
"dependencies": {
70+
"@metamask/delegation-core": "^2.2.1",
71+
"@metamask/utils": "^11.4.0"
6372
}
6473
}

packages/7715-permission-types/src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,10 @@ export type {
1818
RevokeExecutionPermissionResponseResult,
1919
MetaMaskBasePermissionData,
2020
} from './types';
21+
22+
export type { PayeeRule, RedeemerRule, ExpiryRule } from './permissions';
23+
export {
24+
makePermissionDecoderConfigs,
25+
type DeployedContractsByName,
26+
type PermissionDecoderConfig,
27+
} from './permissions';
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { hexToNumber } from '@metamask/utils';
2+
3+
import type { Erc20TokenAllowancePermission } from '../../types';
4+
import { expiryRuleDecoder } from '../rules/expiry';
5+
import { erc20PayeeRuleDecoder } from '../rules/payee';
6+
import { redeemerRuleDecoder } from '../rules/redeemer';
7+
import type {
8+
ChecksumCaveat,
9+
ChecksumEnforcersByChainId,
10+
DecodedPermissionData,
11+
MakePermissionDecoderConfig,
12+
} from '../types';
13+
import {
14+
getByteLength,
15+
getTermsByEnforcer,
16+
splitHex,
17+
UINT256_MAX,
18+
ZERO_32_BYTES,
19+
} from '../utils';
20+
21+
/**
22+
* Builds the configuration for the erc20-token-allowance permission decoder.
23+
*
24+
* @param contractAddresses - Checksummed enforcer addresses for the chain.
25+
* @returns The erc20-token-allowance permission decoder configuration.
26+
*/
27+
export function makeErc20TokenAllowanceDecoderConfig(
28+
contractAddresses: ChecksumEnforcersByChainId,
29+
): MakePermissionDecoderConfig {
30+
const {
31+
timestampEnforcer,
32+
erc20PeriodicEnforcer,
33+
valueLteEnforcer,
34+
nonceEnforcer,
35+
allowedCalldataEnforcer,
36+
redeemerEnforcer,
37+
} = contractAddresses;
38+
39+
return {
40+
permissionType: 'erc20-token-allowance',
41+
contractAddresses,
42+
optionalEnforcers: [
43+
timestampEnforcer, // expiry rule
44+
redeemerEnforcer, // redeemer rule
45+
allowedCalldataEnforcer, // payee rule
46+
],
47+
requiredEnforcers: {
48+
[erc20PeriodicEnforcer]: 1,
49+
[valueLteEnforcer]: 1,
50+
[nonceEnforcer]: 1,
51+
},
52+
rules: [expiryRuleDecoder, redeemerRuleDecoder, erc20PayeeRuleDecoder],
53+
validateAndDecodeData,
54+
};
55+
}
56+
57+
/**
58+
* Decodes erc20-token-allowance permission data from caveats; throws on invalid.
59+
*
60+
* @param caveats - Caveats from the permission context (checksummed).
61+
* @param contractAddresses - Checksummed enforcer addresses for the chain.
62+
* @returns Decoded allowance terms.
63+
*/
64+
function validateAndDecodeData(
65+
caveats: ChecksumCaveat[],
66+
contractAddresses: ChecksumEnforcersByChainId,
67+
): DecodedPermissionData<Erc20TokenAllowancePermission> {
68+
const { erc20PeriodicEnforcer, valueLteEnforcer } = contractAddresses;
69+
70+
const valueLteTerms = getTermsByEnforcer({
71+
caveats,
72+
enforcer: valueLteEnforcer,
73+
});
74+
if (valueLteTerms !== ZERO_32_BYTES) {
75+
throw new Error(`Invalid value-lte terms: must be ${ZERO_32_BYTES}`);
76+
}
77+
78+
const terms = getTermsByEnforcer({
79+
caveats,
80+
enforcer: erc20PeriodicEnforcer,
81+
});
82+
83+
const EXPECTED_TERMS_BYTELENGTH = 116; // 20 + 32 + 32 + 32
84+
85+
if (getByteLength(terms) !== EXPECTED_TERMS_BYTELENGTH) {
86+
throw new Error('Invalid erc20-token-allowance terms: expected 116 bytes');
87+
}
88+
89+
const [tokenAddress, allowanceAmount, periodDurationRaw, startTimeRaw] =
90+
splitHex(terms, [20, 32, 32, 32]);
91+
92+
if (periodDurationRaw.toLowerCase() !== UINT256_MAX) {
93+
throw new Error(
94+
'Invalid erc20-token-allowance terms: periodDuration must be UINT256_MAX',
95+
);
96+
}
97+
98+
const startTime = hexToNumber(startTimeRaw);
99+
100+
if (startTime === 0) {
101+
throw new Error(
102+
'Invalid erc20-token-allowance terms: startTime must be a positive number',
103+
);
104+
}
105+
106+
if (allowanceAmount === ZERO_32_BYTES) {
107+
throw new Error(
108+
'Invalid erc20-token-allowance terms: allowanceAmount must be a positive number',
109+
);
110+
}
111+
112+
return { tokenAddress, allowanceAmount, startTime };
113+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { decodeERC20TokenPeriodTransferTerms } from '@metamask/delegation-core';
2+
import { bigIntToHex } from '@metamask/utils';
3+
4+
import type { Erc20TokenPeriodicPermission } from '../../types';
5+
import { expiryRuleDecoder } from '../rules/expiry';
6+
import { erc20PayeeRuleDecoder } from '../rules/payee';
7+
import { redeemerRuleDecoder } from '../rules/redeemer';
8+
import type {
9+
ChecksumCaveat,
10+
ChecksumEnforcersByChainId,
11+
DecodedPermissionData,
12+
MakePermissionDecoderConfig,
13+
} from '../types';
14+
import {
15+
getTermsByEnforcer,
16+
MAX_PERIOD_DURATION,
17+
ZERO_32_BYTES,
18+
} from '../utils';
19+
20+
/**
21+
* Builds the configuration for the erc20-token-periodic permission decoder.
22+
*
23+
* @param contractAddresses - Checksummed enforcer addresses for the chain.
24+
* @returns The erc20-token-periodic permission decoder configuration.
25+
*/
26+
export function makeErc20TokenPeriodicDecoderConfig(
27+
contractAddresses: ChecksumEnforcersByChainId,
28+
): MakePermissionDecoderConfig {
29+
const {
30+
timestampEnforcer,
31+
erc20PeriodicEnforcer,
32+
valueLteEnforcer,
33+
nonceEnforcer,
34+
allowedCalldataEnforcer,
35+
redeemerEnforcer,
36+
} = contractAddresses;
37+
38+
return {
39+
permissionType: 'erc20-token-periodic',
40+
contractAddresses,
41+
optionalEnforcers: [
42+
timestampEnforcer, // expiry rule
43+
redeemerEnforcer, // redeemer rule
44+
allowedCalldataEnforcer, // payee rule
45+
],
46+
requiredEnforcers: {
47+
[erc20PeriodicEnforcer]: 1,
48+
[valueLteEnforcer]: 1,
49+
[nonceEnforcer]: 1,
50+
},
51+
rules: [expiryRuleDecoder, redeemerRuleDecoder, erc20PayeeRuleDecoder],
52+
validateAndDecodeData,
53+
};
54+
}
55+
56+
/**
57+
* Decodes erc20-token-periodic permission data from caveats; throws on invalid.
58+
*
59+
* @param caveats - Caveats from the permission context (checksummed).
60+
* @param contractAddresses - Checksummed enforcer addresses for the chain.
61+
* @returns Decoded periodic terms.
62+
*/
63+
function validateAndDecodeData(
64+
caveats: ChecksumCaveat[],
65+
contractAddresses: ChecksumEnforcersByChainId,
66+
): DecodedPermissionData<Erc20TokenPeriodicPermission> {
67+
const { erc20PeriodicEnforcer, valueLteEnforcer } = contractAddresses;
68+
69+
const valueLteTerms = getTermsByEnforcer({
70+
caveats,
71+
enforcer: valueLteEnforcer,
72+
});
73+
if (valueLteTerms !== ZERO_32_BYTES) {
74+
throw new Error(`Invalid value-lte terms: must be ${ZERO_32_BYTES}`);
75+
}
76+
77+
const terms = getTermsByEnforcer({
78+
caveats,
79+
enforcer: erc20PeriodicEnforcer,
80+
});
81+
82+
const {
83+
tokenAddress,
84+
periodAmount,
85+
periodDuration,
86+
startDate: startTime,
87+
} = decodeERC20TokenPeriodTransferTerms(terms);
88+
89+
if (periodAmount === 0n) {
90+
throw new Error(
91+
'Invalid erc20-token-periodic terms: periodAmount must be a positive number',
92+
);
93+
}
94+
95+
if (periodDuration === 0) {
96+
throw new Error(
97+
'Invalid erc20-token-periodic terms: periodDuration must be a positive number',
98+
);
99+
}
100+
101+
if (periodDuration > MAX_PERIOD_DURATION) {
102+
throw new Error(
103+
'Invalid erc20-token-periodic terms: periodDuration must be less than or equal to MAX_PERIOD_DURATION',
104+
);
105+
}
106+
107+
if (startTime === 0) {
108+
throw new Error(
109+
'Invalid erc20-token-periodic terms: startTime must be a positive number',
110+
);
111+
}
112+
113+
return {
114+
tokenAddress,
115+
periodAmount: bigIntToHex(periodAmount),
116+
periodDuration,
117+
startTime,
118+
};
119+
}

0 commit comments

Comments
 (0)