-
Notifications
You must be signed in to change notification settings - Fork 89
Feat/naga capacity delegation authsig #1044
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: naga
Are you sure you want to change the base?
Changes from all commits
4628241
109091f
32e9972
1dd24b8
e546516
9e7ee66
94bb078
a77aba0
98fd3ed
9d8280a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| --- | ||
| '@lit-protocol/auth-helpers': patch | ||
| '@lit-protocol/types': patch | ||
| '@lit-protocol/e2e': patch | ||
| --- | ||
|
|
||
| add payment delegation auth sig |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,252 @@ | ||
| import { ethers } from 'ethers'; | ||
|
|
||
| import { InvalidArgumentException, LIT_ABILITY } from '@lit-protocol/constants'; | ||
| import { | ||
| AuthSig, | ||
| PaymentDelegationAuthSigData, | ||
| PaymentDelegationAuthSigParams, | ||
| PaymentDelegationScope, | ||
| } from '@lit-protocol/types'; | ||
|
|
||
| import { generateAuthSig, generateAuthSigWithViem } from './generate-auth-sig'; | ||
| import { LitPaymentDelegationResource } from './resources'; | ||
| import { createSiweMessage } from './siwe/create-siwe-message'; | ||
|
|
||
| const PAYMENT_DELEGATION_SCOPES = new Set<PaymentDelegationScope>([ | ||
| 'encryption_sign', | ||
| 'lit_action', | ||
| 'pkp_sign', | ||
| 'sign_session_key', | ||
| ]); | ||
|
|
||
| type ViemSigner = Parameters<typeof generateAuthSigWithViem>[0]['account']; | ||
|
|
||
| const normalizeDelegateeAddresses = (delegateeAddresses: string[]) => { | ||
| return delegateeAddresses.map((address) => { | ||
| if (!address || typeof address !== 'string') { | ||
| throw new InvalidArgumentException( | ||
| { info: { delegateeAddresses } }, | ||
| 'delegateeAddresses must be a non-empty array of strings' | ||
| ); | ||
| } | ||
|
|
||
| const trimmed = address.trim(); | ||
| const prefixed = trimmed.startsWith('0x') ? trimmed : `0x${trimmed}`; | ||
| const checksummed = ethers.utils.getAddress(prefixed); | ||
|
|
||
| return checksummed.toLowerCase().replace(/^0x/, ''); | ||
| }); | ||
| }; | ||
|
|
||
| const normalizeScopes = (scopes: PaymentDelegationScope[]) => { | ||
| if (!Array.isArray(scopes) || scopes.length === 0) { | ||
| throw new InvalidArgumentException( | ||
| { info: { scopes } }, | ||
| 'scopes must be a non-empty array' | ||
| ); | ||
| } | ||
|
|
||
| const normalizedScopes = Array.from(new Set(scopes)); | ||
|
|
||
| for (const scope of normalizedScopes) { | ||
| if (!PAYMENT_DELEGATION_SCOPES.has(scope)) { | ||
| throw new InvalidArgumentException( | ||
| { info: { scope } }, | ||
| `Unsupported payment delegation scope: ${scope}` | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| return normalizedScopes; | ||
| }; | ||
|
|
||
| const normalizeMaxPrice = ( | ||
| maxPrice: PaymentDelegationAuthSigParams['maxPrice'] | ||
| ) => { | ||
| if (maxPrice === null || maxPrice === undefined) { | ||
| throw new InvalidArgumentException( | ||
| { info: { maxPrice } }, | ||
| 'maxPrice is required' | ||
| ); | ||
| } | ||
|
|
||
| if (typeof maxPrice === 'bigint') { | ||
| if (maxPrice < 0n) { | ||
| throw new InvalidArgumentException( | ||
| { info: { maxPrice } }, | ||
| 'maxPrice must be non-negative' | ||
| ); | ||
| } | ||
| return maxPrice.toString(16); | ||
| } | ||
|
|
||
| if (typeof maxPrice === 'number') { | ||
| if (!Number.isFinite(maxPrice) || maxPrice < 0) { | ||
| throw new InvalidArgumentException( | ||
| { info: { maxPrice } }, | ||
| 'maxPrice must be a finite, non-negative number' | ||
| ); | ||
| } | ||
| return BigInt(Math.trunc(maxPrice)).toString(16); | ||
| } | ||
|
|
||
| const trimmed = maxPrice.trim(); | ||
| if (trimmed.length === 0) { | ||
| throw new InvalidArgumentException( | ||
| { info: { maxPrice } }, | ||
| 'maxPrice must not be empty' | ||
| ); | ||
| } | ||
|
|
||
| if (trimmed.startsWith('0x') || trimmed.startsWith('0X')) { | ||
| return trimmed.slice(2).toLowerCase(); | ||
| } | ||
|
|
||
| if (/^[0-9]+$/.test(trimmed)) { | ||
| return BigInt(trimmed).toString(16); | ||
| } | ||
|
|
||
| if (/^[0-9a-fA-F]+$/.test(trimmed)) { | ||
| return trimmed.toLowerCase(); | ||
| } | ||
|
|
||
| throw new InvalidArgumentException( | ||
| { info: { maxPrice } }, | ||
| 'maxPrice must be a hex or decimal string' | ||
| ); | ||
| }; | ||
|
|
||
| const resolveNonce = async (params: PaymentDelegationAuthSigParams) => { | ||
| if (params.nonce !== undefined) { | ||
| if (typeof params.nonce !== 'string') { | ||
| throw new InvalidArgumentException( | ||
| { info: { nonce: params.nonce } }, | ||
| 'nonce must be a string' | ||
| ); | ||
| } | ||
|
|
||
| const trimmed = params.nonce.trim(); | ||
| if (!trimmed) { | ||
| throw new InvalidArgumentException( | ||
| { info: { nonce: params.nonce } }, | ||
| 'nonce must not be empty' | ||
| ); | ||
| } | ||
| return trimmed; | ||
| } | ||
|
|
||
| if (params.litClient && typeof params.litClient.getContext === 'function') { | ||
| const context = await params.litClient.getContext(); | ||
| if (context?.latestBlockhash) { | ||
| return context.latestBlockhash; | ||
| } | ||
|
|
||
| throw new InvalidArgumentException( | ||
| { info: { context } }, | ||
| 'litClient.getContext() did not return latestBlockhash' | ||
| ); | ||
| } | ||
|
|
||
| throw new InvalidArgumentException( | ||
| { info: { nonce: params.nonce, hasLitClient: Boolean(params.litClient) } }, | ||
| 'nonce is required; provide nonce or litClient' | ||
| ); | ||
| }; | ||
|
|
||
| const resolveSignerAddress = async (params: PaymentDelegationAuthSigParams) => { | ||
| if (params.signerAddress) { | ||
| const prefixed = params.signerAddress.startsWith('0x') | ||
| ? params.signerAddress | ||
| : `0x${params.signerAddress}`; | ||
| return ethers.utils.getAddress(prefixed); | ||
| } | ||
|
|
||
| if ( | ||
| 'getAddress' in params.signer && | ||
| typeof params.signer.getAddress === 'function' | ||
| ) { | ||
| return ethers.utils.getAddress(await params.signer.getAddress()); | ||
| } | ||
|
|
||
| if ( | ||
| 'address' in params.signer && | ||
| typeof (params.signer as { address?: string }).address === 'string' | ||
| ) { | ||
| return ethers.utils.getAddress( | ||
| (params.signer as { address: string }).address | ||
| ); | ||
| } | ||
|
|
||
| throw new InvalidArgumentException( | ||
| { info: { signer: params.signer } }, | ||
| 'signerAddress is required when signer does not expose an address' | ||
| ); | ||
| }; | ||
|
|
||
| export const createPaymentDelegationAuthSig = async ( | ||
| params: PaymentDelegationAuthSigParams | ||
| ): Promise<AuthSig> => { | ||
| if (!params.signer) { | ||
| throw new InvalidArgumentException( | ||
| { info: { signer: params.signer } }, | ||
| 'signer is required' | ||
| ); | ||
| } | ||
|
|
||
| if (!params.delegateeAddresses || params.delegateeAddresses.length === 0) { | ||
| throw new InvalidArgumentException( | ||
| { info: { delegateeAddresses: params.delegateeAddresses } }, | ||
| 'delegateeAddresses must be provided' | ||
| ); | ||
| } | ||
|
|
||
| const signerAddress = await resolveSignerAddress(params); | ||
| const delegateeAddresses = normalizeDelegateeAddresses( | ||
| params.delegateeAddresses | ||
| ); | ||
| const scopes = normalizeScopes(params.scopes); | ||
| const maxPrice = normalizeMaxPrice(params.maxPrice); | ||
| const nonce = await resolveNonce(params); | ||
|
|
||
| const data: PaymentDelegationAuthSigData = { | ||
| delegate_to: delegateeAddresses, | ||
| max_price: maxPrice, | ||
| scopes, | ||
| }; | ||
|
|
||
| const siweMessage = await createSiweMessage({ | ||
| walletAddress: signerAddress, | ||
| nonce, | ||
| expiration: params.expiration, | ||
| domain: params.domain, | ||
| statement: params.statement, | ||
| uri: 'lit:capability:delegation', | ||
| resources: [ | ||
| { | ||
| resource: new LitPaymentDelegationResource('*'), | ||
| ability: LIT_ABILITY.PaymentDelegation, | ||
| data, | ||
| }, | ||
| ], | ||
| }); | ||
|
|
||
| if ( | ||
| !('getAddress' in params.signer) && | ||
| 'address' in params.signer && | ||
| typeof (params.signer as { address?: string }).address === 'string' | ||
| ) { | ||
| return generateAuthSigWithViem({ | ||
| account: params.signer as ViemSigner, | ||
| toSign: siweMessage, | ||
| address: signerAddress, | ||
| }); | ||
| } | ||
|
|
||
| return generateAuthSig({ | ||
| signer: params.signer, | ||
| toSign: siweMessage, | ||
| address: signerAddress, | ||
| }); | ||
| }; | ||
|
|
||
| export { PAYMENT_DELEGATION_SCOPES }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -52,28 +52,27 @@ export function createEnvVars(): EnvVars { | |
| const selectedNetwork = network.includes('local') ? 'local' : 'live'; | ||
|
|
||
| // 2. Get private key | ||
| let privateKey: `0x${string}`; | ||
| let privateKey: `0x${string}` | undefined; | ||
| if (network.includes('local')) { | ||
| Object.assign(testEnv.local, { type: 'local' }); | ||
| privateKey = process.env[testEnv.local.key]!! as `0x${string}`; | ||
| privateKey = process.env[testEnv.local.key] as `0x${string}`; | ||
| } else { | ||
| Object.assign(testEnv.live, { type: 'live' }); | ||
| const liveKey = | ||
| network === 'naga' && process.env['LIVE_MASTER_ACCOUNT_NAGA'] | ||
| ? 'LIVE_MASTER_ACCOUNT_NAGA' | ||
| : testEnv.live.key; | ||
| privateKey = process.env[liveKey]!! as `0x${string}`; | ||
| const legacyKey = testEnv.live.key; | ||
| const scopedKey = `LIVE_MASTER_ACCOUNT_${network | ||
| .toUpperCase() | ||
| .replace(/-/g, '_')}`; | ||
| privateKey = | ||
| (process.env[legacyKey] as `0x${string}` | undefined) ?? | ||
| (process.env[scopedKey] as `0x${string}` | undefined); | ||
| } | ||
|
|
||
| if (!privateKey) { | ||
| const expectedKey = | ||
| network === 'naga' | ||
| ? 'LIVE_MASTER_ACCOUNT_NAGA or LIVE_MASTER_ACCOUNT' | ||
| : selectedNetwork === 'local' | ||
| ? 'LOCAL_MASTER_ACCOUNT' | ||
| : 'LIVE_MASTER_ACCOUNT'; | ||
| const scopedKey = `LIVE_MASTER_ACCOUNT_${network | ||
| .toUpperCase() | ||
| .replace(/-/g, '_')}`; | ||
|
Comment on lines
+62
to
+73
|
||
| throw new Error( | ||
| `❌ ${expectedKey} env var is not set for NETWORK=${network}.` | ||
| `❌ You are on "${selectedNetwork}" environment, network ${network}. We are expecting ${testEnv.live.key} or ${scopedKey} to be set.` | ||
| ); | ||
| } | ||
|
Comment on lines
70
to
77
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| import { registerPaymentDelegationAuthSigTicketSuite } from './payment-delegation-auth-sig.suite'; | ||
|
|
||
| registerPaymentDelegationAuthSigTicketSuite(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The payment-delegation module contains complex validation logic (normalizeDelegateeAddresses, normalizeScopes, normalizeMaxPrice, resolveNonce, resolveSignerAddress) but lacks unit tests. Given that this package has existing unit test coverage (e.g., auth-config-builder.spec.ts), consider adding unit tests for the validation functions to ensure edge cases are handled correctly, especially for input validation and error conditions.