Skip to content

Commit f72688b

Browse files
committed
feat(card): migrate delegation methods to CardController
1 parent dcad69f commit f72688b

10 files changed

Lines changed: 403 additions & 231 deletions

File tree

app/components/UI/Card/hooks/useCardDelegation.test.ts

Lines changed: 99 additions & 116 deletions
Large diffs are not rendered by default.

app/components/UI/Card/hooks/useCardDelegation.ts

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ import {
1111
type Transaction,
1212
} from '@metamask/keyring-api';
1313
import Engine from '../../../../core/Engine';
14+
import { encodeErc20ApproveCalldata } from '../../../../core/Engine/controllers/card-controller/utils/encodeErc20ApproveCalldata';
1415
import TransactionTypes from '../../../../core/TransactionTypes';
1516
import Logger from '../../../../util/Logger';
1617
import { selectSelectedInternalAccountByScope } from '../../../../selectors/multichainAccounts/accounts';
17-
import { useCardSDK } from '../sdk';
1818
import { CardNetwork, CardFundingToken } from '../types';
1919
import { useAnalytics } from '../../../hooks/useAnalytics/useAnalytics';
2020
import { useEnsureCardNetworkExists } from './useEnsureCardNetworkExists';
@@ -76,8 +76,8 @@ interface SignCardMessageResult {
7676
* Flow: Token -> Signature -> Approval Transaction -> Completion
7777
*/
7878
export const useCardDelegation = (token?: CardFundingToken | null) => {
79-
const { sdk } = useCardSDK();
80-
const { KeyringController, TransactionController } = Engine.context;
79+
const { KeyringController, TransactionController, CardController } =
80+
Engine.context;
8181
const { ensureNetworkExists } = useEnsureCardNetworkExists();
8282
const selectAccountByScope = useSelector(
8383
selectSelectedInternalAccountByScope,
@@ -133,7 +133,7 @@ export const useCardDelegation = (token?: CardFundingToken | null) => {
133133
signatureMessage: string,
134134
delegationJWTToken: string,
135135
) => {
136-
if (!sdk || !token?.delegationContract) {
136+
if (!token?.delegationContract) {
137137
throw new Error('Missing token configuration');
138138
}
139139

@@ -154,7 +154,7 @@ export const useCardDelegation = (token?: CardFundingToken | null) => {
154154
token.decimals ?? 18,
155155
).toString();
156156

157-
const transactionData = sdk.encodeApproveTransaction(
157+
const transactionData = encodeErc20ApproveCalldata(
158158
token.delegationContract,
159159
amountInMinimalUnits,
160160
);
@@ -193,7 +193,7 @@ export const useCardDelegation = (token?: CardFundingToken | null) => {
193193
async (transactionMeta) => {
194194
if (transactionMeta.status === TransactionStatus.confirmed) {
195195
try {
196-
await sdk.completeDelegation({
196+
await CardController.approveFunding({
197197
address,
198198
network: params.network,
199199
currency: params.currency.toLowerCase(),
@@ -234,7 +234,7 @@ export const useCardDelegation = (token?: CardFundingToken | null) => {
234234
rethrowAsUserCancelledIfApplicable(error);
235235
}
236236
},
237-
[sdk, token, TransactionController, ensureNetworkExists],
237+
[token, TransactionController, ensureNetworkExists, CardController],
238238
);
239239

240240
/**
@@ -256,7 +256,7 @@ export const useCardDelegation = (token?: CardFundingToken | null) => {
256256
signatureMessage: string,
257257
delegationJWTToken: string,
258258
) => {
259-
if (!sdk || !token?.delegationContract) {
259+
if (!token?.delegationContract) {
260260
throw new Error('Missing token configuration');
261261
}
262262
if (!token?.stagingTokenAddress && !token?.address) {
@@ -371,7 +371,7 @@ export const useCardDelegation = (token?: CardFundingToken | null) => {
371371
});
372372

373373
// Complete the delegation with the backend API after confirmation
374-
await sdk.completeDelegation({
374+
await CardController.approveFunding({
375375
address,
376376
network: params.network,
377377
currency: params.currency.toLowerCase(),
@@ -391,7 +391,7 @@ export const useCardDelegation = (token?: CardFundingToken | null) => {
391391
rethrowAsUserCancelledIfApplicable(error);
392392
}
393393
},
394-
[sdk, token],
394+
[token, CardController],
395395
);
396396

397397
const signSolanaMessage = useCallback(
@@ -438,10 +438,6 @@ export const useCardDelegation = (token?: CardFundingToken | null) => {
438438
*/
439439
const submitDelegation = useCallback(
440440
async (params: DelegationParams) => {
441-
if (!sdk) {
442-
throw new Error('Card SDK not available');
443-
}
444-
445441
setState({ isLoading: true, error: null });
446442

447443
const metricsProps = {
@@ -473,13 +469,13 @@ export const useCardDelegation = (token?: CardFundingToken | null) => {
473469
throw new Error('No account found');
474470
}
475471

476-
// Step 1: Generate delegation token (pass faucet flag if user needs gas)
477-
const { token: delegationJWTToken, nonce } =
478-
await sdk.generateDelegationToken(
479-
params.network,
472+
// Step 1: Delegation session (pass faucet flag if user needs gas)
473+
const { delegationToken: delegationJWTToken, nonce } =
474+
await CardController.fetchDelegationChallenge({
475+
network: params.network,
480476
address,
481-
needsFaucet,
482-
);
477+
faucet: needsFaucet,
478+
});
483479

484480
// Step 2: Generate and sign SIWE message
485481
const signatureMessage = generateSignatureMessage(
@@ -548,7 +544,6 @@ export const useCardDelegation = (token?: CardFundingToken | null) => {
548544
}
549545
},
550546
[
551-
sdk,
552547
selectAccountByScope,
553548
generateSignatureMessage,
554549
KeyringController,
@@ -559,6 +554,7 @@ export const useCardDelegation = (token?: CardFundingToken | null) => {
559554
trackEvent,
560555
createEventBuilder,
561556
needsFaucet,
557+
CardController,
562558
],
563559
);
564560

app/components/UI/Card/sdk/CardSDK.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1646,13 +1646,6 @@ export class CardSDK {
16461646
},
16471647
);
16481648

1649-
encodeApproveTransaction = (spender: string, value: string): string => {
1650-
const approvalInterface = new ethers.utils.Interface([
1651-
'function approve(address spender, uint256 value)',
1652-
]);
1653-
return approvalInterface.encodeFunctionData('approve', [spender, value]);
1654-
};
1655-
16561649
/**
16571650
* Validate delegation settings response
16581651
*/

app/core/Engine/controllers/card-controller/CardController.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1514,6 +1514,50 @@ describe('CardController — data pass-throughs', () => {
15141514
});
15151515
});
15161516

1517+
describe('fetchDelegationChallenge', () => {
1518+
it('delegates to provider.fetchDelegationChallenge', async () => {
1519+
const mockFetch = jest.fn().mockResolvedValue({
1520+
delegationToken: 'jwt',
1521+
nonce: 'nonce-1',
1522+
expiresAt: '2099-01-01',
1523+
});
1524+
const provider = buildMockProvider({
1525+
fetchDelegationChallenge: mockFetch,
1526+
});
1527+
const { controller } = buildAuthenticatedController(provider);
1528+
1529+
const result = await controller.fetchDelegationChallenge({
1530+
network: 'linea',
1531+
address: '0xabc',
1532+
faucet: true,
1533+
});
1534+
1535+
expect(mockFetch).toHaveBeenCalledWith(
1536+
{ network: 'linea', address: '0xabc', faucet: true },
1537+
mockTokenSet,
1538+
);
1539+
expect(result).toStrictEqual({
1540+
delegationToken: 'jwt',
1541+
nonce: 'nonce-1',
1542+
expiresAt: '2099-01-01',
1543+
});
1544+
});
1545+
1546+
it('throws when provider does not support fetchDelegationChallenge', async () => {
1547+
const provider = buildMockProvider({
1548+
fetchDelegationChallenge: undefined,
1549+
});
1550+
const { controller } = buildAuthenticatedController(provider);
1551+
1552+
await expect(
1553+
controller.fetchDelegationChallenge({
1554+
network: 'linea',
1555+
address: '0xabc',
1556+
}),
1557+
).rejects.toThrow('Delegation challenge not supported');
1558+
});
1559+
});
1560+
15171561
describe('createGoogleWalletProvisioningRequest', () => {
15181562
it('delegates to provider', async () => {
15191563
const mockCreate = jest

app/core/Engine/controllers/card-controller/CardController.ts

Lines changed: 17 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { BaseController, type StateMetadata } from '@metamask/base-controller';
2-
import { TransactionType } from '@metamask/transaction-controller';
3-
import { numberToHex, type Hex, type Json } from '@metamask/utils';
2+
import { type Json } from '@metamask/utils';
43
import Logger from '../../../../util/Logger';
54
import {
65
CARD_CONTROLLER_NAME,
@@ -30,9 +29,9 @@ import {
3029
type CashbackWithdrawEstimationResponse,
3130
type CashbackWithdrawParams,
3231
type CashbackWithdrawResponse,
32+
type DelegationChallengeResponse,
3333
type FundingApprovalParams,
3434
type ICardProvider,
35-
type WalletOperations,
3635
} from './provider-types';
3736
import { CardTokenStore } from './CardTokenStore';
3837
import { isEthAccount } from '../../../Multichain/utils';
@@ -838,39 +837,23 @@ export class CardController extends BaseController<
838837
'Funding approval not supported',
839838
);
840839
}
841-
const wallet = this.#buildWalletOperations();
842-
return provider.approveFunding(params, tokens, wallet);
840+
return provider.approveFunding(params, tokens);
843841
}
844842

845-
#buildWalletOperations(): WalletOperations {
846-
return {
847-
signMessage: async (address: string, message: string) => {
848-
const hex = `0x${Buffer.from(message, 'utf8').toString('hex')}`;
849-
return this.messenger.call('KeyringController:signPersonalMessage', {
850-
data: hex,
851-
from: address,
852-
});
853-
},
854-
submitTransaction: async (txParams, chainId) => {
855-
const chainNumber = parseInt(chainId.split(':')[1], 10);
856-
const hexChainId = numberToHex(chainNumber) as Hex;
857-
const networkClientId = this.messenger.call(
858-
'NetworkController:findNetworkClientIdByChainId',
859-
hexChainId,
860-
);
861-
const { result } = await this.messenger.call(
862-
'TransactionController:addTransaction',
863-
txParams,
864-
{
865-
networkClientId,
866-
origin: 'metamask',
867-
type: TransactionType.tokenMethodApprove,
868-
requireApproval: true,
869-
},
870-
);
871-
return result;
872-
},
873-
};
843+
async fetchDelegationChallenge(params: {
844+
network: string;
845+
address: string;
846+
faucet?: boolean;
847+
}): Promise<DelegationChallengeResponse> {
848+
const tokens = await this.requireValidTokens();
849+
const provider = this.getActiveProvider();
850+
if (!provider.fetchDelegationChallenge) {
851+
throw new CardProviderError(
852+
CardProviderErrorCode.Unknown,
853+
'Delegation challenge not supported',
854+
);
855+
}
856+
return provider.fetchDelegationChallenge(params, tokens);
874857
}
875858

876859
// -- Push Provisioning --

app/core/Engine/controllers/card-controller/provider-types.ts

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -224,20 +224,22 @@ export function emptyCardHomeData(): CardHomeData {
224224

225225
// -- Funding --
226226

227-
export interface WalletOperations {
228-
signMessage(address: string, message: string): Promise<string>;
229-
submitTransaction(
230-
params: { to: string; data: string; from: string },
231-
chainId: CaipChainId,
232-
): Promise<string>;
233-
}
234-
235227
export interface FundingApprovalParams {
236228
address: string;
237229
amount: string;
238230
currency: string;
239231
network: string;
240-
faucet?: boolean;
232+
txHash: string;
233+
sigHash: string;
234+
sigMessage: string;
235+
token: string;
236+
}
237+
238+
/** Response from initiating a delegation session (GET `/v1/delegation/token`). */
239+
export interface DelegationChallengeResponse {
240+
delegationToken: string;
241+
nonce: string;
242+
expiresAt: string;
241243
}
242244

243245
export interface CardFundingOption {
@@ -360,10 +362,17 @@ export interface ICardProvider {
360362
): Promise<void>;
361363
getFundingConfig?(tokens: CardAuthTokens): Promise<CardFundingConfig>;
362364

365+
/**
366+
* Fetches a short-lived delegation session (nonce + JWT) before SIWE + on-chain approve.
367+
*/
368+
fetchDelegationChallenge?(
369+
params: { network: string; address: string; faucet?: boolean },
370+
tokens: CardAuthTokens,
371+
): Promise<DelegationChallengeResponse>;
372+
363373
approveFunding?(
364374
params: FundingApprovalParams,
365375
tokens: CardAuthTokens,
366-
wallet: WalletOperations,
367376
): Promise<void>;
368377

369378
getCashbackWallet?(tokens: CardAuthTokens): Promise<CashbackWalletResponse>;

0 commit comments

Comments
 (0)