Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/add-deploy-immediately-flag.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@crossmint/wallets-sdk": minor
---

feat(wallets): add `deployImmediately` flag to EVM `addSigner`

When registering a delegated signer on an EVM wallet, the SDK now sends
`deployImmediately: true` by default, causing the API to return an on-chain
registration transaction instead of the lazy signature-request flow. This can be
overridden by passing `{ deployImmediately: false }` in the options.
1 change: 1 addition & 0 deletions packages/wallets/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export type RegisterSignerParams = {
signer: SignerLocator | RegisterSignerPasskeyParams | SignerConfigForChain<Chain>;
chain?: RegisterSignerChain;
scopes?: Scope[];
deployImmediately?: boolean;
};
export type RegisterSignerResponse = DelegatedSignerV2025DtoClass | WalletsV2025ControllerCreateDelegatedSigner2Error;
export type RemoveSignerParams = {
Expand Down
22 changes: 22 additions & 0 deletions packages/wallets/src/utils/signer-mapping.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,28 @@ describe("getPendingSignerOperation", () => {
});
});

it("returns a transaction operation for EVM deployImmediately chain entries", () => {
const apiSigner = {
type: "external-wallet",
address: "0x123",
locator: "external-wallet:0x123",
chains: {
"base-sepolia": {
id: "tx-456",
status: "awaiting-approval",
onChain: { userOperation: "0xabc", userOperationHash: "0xdef" },
chainType: "evm",
walletType: "smart",
},
},
} as unknown as APISigner;

expect(getPendingSignerOperation(apiSigner, "base-sepolia")).toEqual({
type: "transaction",
id: "tx-456",
});
});

it("returns null for failed signer registrations", () => {
const apiSigner = {
type: "email",
Expand Down
3 changes: 2 additions & 1 deletion packages/wallets/src/utils/signer-mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ export function getPendingSignerOperation(
if ("chains" in apiSigner && apiSigner.chains != null) {
const chainEntry = apiSigner.chains[chain];
if (chainEntry != null && (chainEntry.status === "pending" || chainEntry.status === "awaiting-approval")) {
return { type: "signature", id: chainEntry.id };
const operationType = "onChain" in chainEntry ? "transaction" : "signature";
return { type: operationType, id: chainEntry.id };
}
}

Expand Down
3 changes: 2 additions & 1 deletion packages/wallets/src/wallets/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export type SignatureInputOptions = PrepareOnly;

export type AddSignerOptions = PrepareOnly & {
scopes?: Scope[];
deployImmediately?: boolean;
};

export type RemoveSignerOptions = PrepareOnly;
Expand All @@ -52,7 +53,7 @@ export type MigrateOptions = Partial<PrepareOnly> & {

export type AddSignerReturnType<C extends Chain> = C extends "solana" | "stellar"
? Signer & { transactionId: string }
: Signer & { signatureId?: string };
: Signer & { signatureId?: string; transactionId?: string };

export type RemoveSignerReturnType = { transactionId: string; status?: "success" };

Expand Down
95 changes: 95 additions & 0 deletions packages/wallets/src/wallets/wallet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -658,13 +658,108 @@ describe("Wallet - addSigner()", () => {
expect.objectContaining({
signer: "external-wallet:0x456",
chain: "base-sepolia",
deployImmediately: true,
})
);
expect(result.type).toBe("external-wallet");
expect(result.locator).toBe("external-wallet:0x456");
expect(result.status).toBe("success");
});

it("passes deployImmediately: false when explicitly set", async () => {
const mockRegisterResponse = {
type: "external-wallet",
address: "0x456",
locator: "external-wallet:0x456",
chains: {
"base-sepolia": {
id: "sig-123",
status: "success",
},
},
};

mockApiClient.registerSigner.mockResolvedValue(mockRegisterResponse as any);

await evmWallet.addSigner(
{ type: "external-wallet", address: "0x456" },
{ prepareOnly: false, deployImmediately: false }
);

expect(mockApiClient.registerSigner).toHaveBeenCalledWith(
"me:evm:smart",
expect.objectContaining({
signer: "external-wallet:0x456",
chain: "base-sepolia",
deployImmediately: false,
})
);
});

it("approves transaction when deployImmediately response has onChain field", async () => {
const mockRegisterResponse = {
type: "external-wallet",
address: "0x456",
locator: "external-wallet:0x456",
chains: {
"base-sepolia": {
id: "tx-789",
status: "awaiting-approval",
onChain: { userOperation: "0xabc", userOperationHash: "0xdef" },
chainType: "evm",
walletType: "smart",
},
},
};

const mockTransactionResponse = {
id: "tx-789",
status: "success",
onChain: {
txId: "0xhash",
explorerLink: "https://basescan.org/tx/0xhash",
},
};

mockApiClient.registerSigner.mockResolvedValue(mockRegisterResponse as any);
mockApiClient.getTransaction.mockResolvedValue(mockTransactionResponse as any);

const addPromise = evmWallet.addSigner({ type: "external-wallet", address: "0x456" });
await vi.runAllTimersAsync();
const result = await addPromise;

expect(result.type).toBe("external-wallet");
expect(result.status).toBe("success");
});

it("returns transactionId with prepareOnly for deployImmediately flow", async () => {
const mockRegisterResponse = {
type: "external-wallet",
address: "0x456",
locator: "external-wallet:0x456",
chains: {
"base-sepolia": {
id: "tx-789",
status: "awaiting-approval",
onChain: { userOperation: "0xabc", userOperationHash: "0xdef" },
chainType: "evm",
walletType: "smart",
},
},
};

mockApiClient.registerSigner.mockResolvedValue(mockRegisterResponse as any);

const result = await evmWallet.addSigner(
{ type: "external-wallet", address: "0x456" },
{ prepareOnly: true }
);

expect(result.transactionId).toBe("tx-789");
expect(result.type).toBe("external-wallet");
expect(result.status).toBe("awaiting-approval");
});

it("returns signatureId with prepareOnly", async () => {
const mockRegisterResponse = {
type: "external-wallet",
Expand Down
4 changes: 4 additions & 0 deletions packages/wallets/src/wallets/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -774,10 +774,14 @@ export class Wallet<C extends Chain> {
}
: getSignerLocator(resolvedSigner);

const isEvm = this.chain !== "solana" && this.chain !== "stellar";
const deployImmediately = isEvm ? options?.deployImmediately ?? true : undefined;

const response = await this.#apiClient.registerSigner(this.walletLocator, {
signer: signerInput as RegisterSignerParams["signer"],
chain: this.getSignerRegistrationChain(),
...(options?.scopes != null && { scopes: options.scopes }),
...(deployImmediately != null && { deployImmediately }),
});

if ("error" in response) {
Expand Down
Loading