From 37b6798b979775c0819dfb2e7a0b7e334abf0fc5 Mon Sep 17 00:00:00 2001 From: Andrew Min Date: Fri, 21 Nov 2025 19:01:02 -0500 Subject: [PATCH 1/9] low hanging fruit: type annotations --- packages/iframe-stamper/src/index.ts | 63 ++++++++++++++++++++++++---- 1 file changed, 54 insertions(+), 9 deletions(-) diff --git a/packages/iframe-stamper/src/index.ts b/packages/iframe-stamper/src/index.ts index e8b974b87..40d867000 100644 --- a/packages/iframe-stamper/src/index.ts +++ b/packages/iframe-stamper/src/index.ts @@ -178,6 +178,11 @@ export class IframeStamper { /** * Creates a new iframe stamper. This function _does not_ insert the iframe in the DOM. * Call `.init()` to insert the iframe element in the DOM. + * @param {TIframeStamperConfig} config - Configuration object for the iframe stamper + * @throws {Error} When running in non-browser environment + * @throws {Error} When MessageChannel is not supported + * @throws {Error} When iframeContainer is not provided + * @throws {Error} When iframe element with the same ID already exists */ constructor(config: TIframeStamperConfig) { if (typeof window === "undefined") { @@ -232,6 +237,11 @@ export class IframeStamper { this.pendingRequests = new Map(); } + /** + * Handles incoming messages from the iframe via MessageChannel + * @param {MessageEvent} event - Message event from the iframe + * @returns {void} + */ onMessageHandler(event: MessageEvent): void { const { type, value, requestId } = event.data || {}; @@ -273,7 +283,9 @@ export class IframeStamper { /** * Inserts the iframe on the page and returns a promise resolving to the iframe's public key - * @param dangerouslyOverrideIframeKeyTtl Optional TTL override for the iframe's embedded key (default 48 hours). Only use this if you are intentional about the security implications. + * @param {number} [dangerouslyOverrideIframeKeyTtl] - Optional TTL override for the iframe's embedded key (default 48 hours). Only use this if you are intentional about the security implications. + * @returns {Promise} The iframe's public key + * @throws {Error} When contentWindow or contentWindow.postMessage does not exist */ async init( dangerouslyOverrideIframeKeyTtl?: number | undefined, @@ -315,7 +327,8 @@ export class IframeStamper { } /** - * Removes the iframe from the DOM + * Removes the iframe from the DOM and cleans up all resources + * @returns {void} */ clear() { this.messageChannel?.port1?.close(); @@ -326,6 +339,7 @@ export class IframeStamper { /** * Returns the public key, or `null` if the underlying iframe isn't properly initialized. + * @returns {string | null} The iframe's public key or null */ publicKey(): string | null { return this.iframePublicKey; @@ -334,6 +348,7 @@ export class IframeStamper { /** * Returns the public key, or `null` if the underlying iframe isn't properly initialized. * This differs from the above in that it reaches out to the live iframe to see if an embedded key exists. + * @returns {Promise} The embedded public key or null */ async getEmbeddedPublicKey(): Promise { const publicKey = await this.createRequest( @@ -346,6 +361,7 @@ export class IframeStamper { /** * Clears the embedded key within an iframe. + * @returns {Promise} Returns null on success */ async clearEmbeddedKey(): Promise { await this.createRequest(IframeEventType.ClearEmbeddedKey); @@ -358,7 +374,7 @@ export class IframeStamper { * Creates a new embedded key within an iframe. If an embedded key already exists, this will return it. * This is primarily to be used in conjunction with `clearEmbeddedKey()`: after an embedded key is cleared, * this can be used to create a new one. - * @return {string | null} the newly created embedded public key. + * @returns {Promise} The newly created embedded public key */ async initEmbeddedKey(): Promise { const publicKey = await this.createRequest( @@ -371,9 +387,10 @@ export class IframeStamper { /** * Generic function to abstract away request creation - * @param type - * @param payload - * @returns expected shape + * @template T + * @param {IframeEventType} type - The type of iframe event to send + * @param {any} [payload={}] - Optional payload data to send with the request + * @returns {Promise} Promise resolving to the expected response shape */ private createRequest( type: IframeEventType, @@ -401,6 +418,8 @@ export class IframeStamper { * The bundle should be encrypted to the iframe's initial public key * Encryption should be performed with HPKE (RFC 9180). * This is used during recovery and auth flows. + * @param {string} bundle - The encrypted credential bundle to inject + * @returns {Promise} Returns true on successful injection */ async injectCredentialBundle(bundle: string): Promise { return this.createRequest(IframeEventType.InjectCredentialBundle, { @@ -414,16 +433,23 @@ export class IframeStamper { * Encryption should be performed with HPKE (RFC 9180). * The key format to encode the private key in after it's exported and decrypted: HEXADECIMAL or SOLANA. Defaults to HEXADECIMAL. * This is used during the private key export flow. + * @param {string} bundle - The encrypted export bundle to inject + * @param {string} organizationId - The organization ID + * @param {KeyFormat} [keyFormat] - The key format (HEXADECIMAL or SOLANA). Defaults to HEXADECIMAL + * @param {string} [address] - Address corresponding to the key bundle (case sensitive) + * @returns {Promise} Returns true on successful injection */ async injectKeyExportBundle( bundle: string, organizationId: string, keyFormat?: KeyFormat, + address?: string, // address corresponding to the key bundle. Note that this is case sensitive ): Promise { return this.createRequest(IframeEventType.InjectKeyExportBundle, { value: bundle, keyFormat, organizationId, + address, }); } @@ -432,6 +458,9 @@ export class IframeStamper { * The bundle should be encrypted to the iframe's initial public key * Encryption should be performed with HPKE (RFC 9180). * This is used during the wallet export flow. + * @param {string} bundle - The encrypted wallet export bundle to inject + * @param {string} organizationId - The organization ID + * @returns {Promise} Returns true on successful injection */ async injectWalletExportBundle( bundle: string, @@ -449,6 +478,10 @@ export class IframeStamper { /** * Function to inject an import bundle into the iframe * This is used to initiate either the wallet import flow or the private key import flow. + * @param {string} bundle - The import bundle to inject + * @param {string} organizationId - The organization ID + * @param {string} userId - The user ID + * @returns {Promise} Returns true on successful injection */ async injectImportBundle( bundle: string, @@ -467,6 +500,7 @@ export class IframeStamper { * The bundle should be encrypted to Turnkey's Signer enclave's initial public key * Encryption should be performed with HPKE (RFC 9180). * This is used during the wallet import flow. + * @returns {Promise} The encrypted wallet bundle */ async extractWalletEncryptedBundle(): Promise { return this.createRequest( @@ -480,6 +514,8 @@ export class IframeStamper { * Encryption should be performed with HPKE (RFC 9180). * The key format to encode the private key in before it's encrypted and imported: HEXADECIMAL or SOLANA. Defaults to HEXADECIMAL. * This is used during the private key import flow. + * @param {KeyFormat} [keyFormat] - The key format (HEXADECIMAL or SOLANA). Defaults to HEXADECIMAL + * @returns {Promise} The encrypted key bundle */ async extractKeyEncryptedBundle(keyFormat?: KeyFormat): Promise { return this.createRequest( @@ -491,6 +527,8 @@ export class IframeStamper { /** * Function to apply settings on allowed parameters in the iframe * This is used to style the HTML element used for plaintext in wallet and private key import. + * @param {TIframeSettings} settings - The settings object containing styles to apply + * @returns {Promise} Returns true on successful application */ async applySettings(settings: TIframeSettings): Promise { return this.createRequest(IframeEventType.ApplySettings, { @@ -500,6 +538,9 @@ export class IframeStamper { /** * Function to sign a payload with the underlying iframe + * @param {string} payload - The payload to sign + * @returns {Promise} Object containing stamp header name and value + * @throws {Error} When iframe public key is null (init() not called/awaited) */ async stamp(payload: string): Promise { if (this.iframePublicKey === null) { @@ -514,8 +555,10 @@ export class IframeStamper { } /** - * Function to sign a transaction using an embedded private key in-memory within an iframe + * Function to sign a message using an embedded private key in-memory within an iframe * Returns the signed message string + * @param {TSignableMessage} message - The message to sign with type (Ethereum or Solana) + * @returns {Promise} The signed message string */ async signMessage(message: TSignableMessage): Promise { return this.createRequest(IframeEventType.SignMessage, { @@ -524,8 +567,10 @@ export class IframeStamper { } /** - * Function to sign a message using an embedded private key in-memory within an iframe + * Function to sign a transaction using an embedded private key in-memory within an iframe * Returns the signed, serialized transaction payload + * @param {TSignableTransaction} transaction - The transaction to sign with type (Ethereum or Solana) + * @returns {Promise} The signed, serialized transaction payload */ async signTransaction(transaction: TSignableTransaction): Promise { return this.createRequest(IframeEventType.SignTransaction, { @@ -535,7 +580,7 @@ export class IframeStamper { /** * Function to clear the iframe's in-memory embedded private key. For now, we assume that there will be only one private key at most. - * Returns boolean + * @returns {Promise} Returns true on successful clearing */ async clearEmbeddedPrivateKey(): Promise { return this.createRequest(IframeEventType.clearEmbeddedPrivateKey); From dd5fd82a45ec513a7ba9965388ff23d1d8736d20 Mon Sep 17 00:00:00 2001 From: Andrew Min Date: Fri, 21 Nov 2025 19:04:08 -0500 Subject: [PATCH 2/9] update key import and signing methods --- packages/iframe-stamper/src/index.ts | 50 +++++++++++++++++----------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/packages/iframe-stamper/src/index.ts b/packages/iframe-stamper/src/index.ts index 40d867000..79a45a9e6 100644 --- a/packages/iframe-stamper/src/index.ts +++ b/packages/iframe-stamper/src/index.ts @@ -191,7 +191,7 @@ export class IframeStamper { if (typeof MessageChannel === "undefined") { throw new Error( - "Cannot initialize iframe without MessageChannel support", + "Cannot initialize iframe without MessageChannel support" ); } @@ -202,7 +202,7 @@ export class IframeStamper { if (this.container.querySelector(`#${config.iframeElementId}`)) { throw new Error( - `Iframe element with ID ${config.iframeElementId} already exists`, + `Iframe element with ID ${config.iframeElementId} already exists` ); } @@ -288,7 +288,7 @@ export class IframeStamper { * @throws {Error} When contentWindow or contentWindow.postMessage does not exist */ async init( - dangerouslyOverrideIframeKeyTtl?: number | undefined, + dangerouslyOverrideIframeKeyTtl?: number | undefined ): Promise { return new Promise((resolve, reject) => { this.container.appendChild(this.iframe); @@ -297,8 +297,8 @@ export class IframeStamper { if (!this.iframe.contentWindow?.postMessage) { reject( new Error( - "contentWindow or contentWindow.postMessage does not exist", - ), + "contentWindow or contentWindow.postMessage does not exist" + ) ); return; } @@ -309,7 +309,7 @@ export class IframeStamper { dangerouslyOverrideIframeKeyTtl: dangerouslyOverrideIframeKeyTtl, }, this.iframeOrigin, - [this.messageChannel.port2], + [this.messageChannel.port2] ); }); @@ -352,7 +352,7 @@ export class IframeStamper { */ async getEmbeddedPublicKey(): Promise { const publicKey = await this.createRequest( - IframeEventType.GetEmbeddedPublicKey, + IframeEventType.GetEmbeddedPublicKey ); this.iframePublicKey = publicKey; @@ -378,7 +378,7 @@ export class IframeStamper { */ async initEmbeddedKey(): Promise { const publicKey = await this.createRequest( - IframeEventType.InitEmbeddedKey, + IframeEventType.InitEmbeddedKey ); this.iframePublicKey = publicKey; @@ -394,7 +394,7 @@ export class IframeStamper { */ private createRequest( type: IframeEventType, - payload: any = {}, + payload: any = {} ): Promise { return new Promise((resolve, reject) => { const requestId = generateUUID(); @@ -435,15 +435,15 @@ export class IframeStamper { * This is used during the private key export flow. * @param {string} bundle - The encrypted export bundle to inject * @param {string} organizationId - The organization ID - * @param {KeyFormat} [keyFormat] - The key format (HEXADECIMAL or SOLANA). Defaults to HEXADECIMAL - * @param {string} [address] - Address corresponding to the key bundle (case sensitive) + * @param {KeyFormat} keyFormat - [Optional] The key format (HEXADECIMAL or SOLANA). Defaults to HEXADECIMAL + * @param {string} address - [Optional] Address corresponding to the key bundle (case sensitive) * @returns {Promise} Returns true on successful injection */ async injectKeyExportBundle( bundle: string, organizationId: string, keyFormat?: KeyFormat, - address?: string, // address corresponding to the key bundle. Note that this is case sensitive + address?: string ): Promise { return this.createRequest(IframeEventType.InjectKeyExportBundle, { value: bundle, @@ -464,14 +464,14 @@ export class IframeStamper { */ async injectWalletExportBundle( bundle: string, - organizationId: string, + organizationId: string ): Promise { return this.createRequest( IframeEventType.InjectWalletExportBundle, { value: bundle, organizationId, - }, + } ); } @@ -486,7 +486,7 @@ export class IframeStamper { async injectImportBundle( bundle: string, organizationId: string, - userId: string, + userId: string ): Promise { return this.createRequest(IframeEventType.InjectImportBundle, { value: bundle, @@ -504,7 +504,7 @@ export class IframeStamper { */ async extractWalletEncryptedBundle(): Promise { return this.createRequest( - IframeEventType.ExtractWalletEncryptedBundle, + IframeEventType.ExtractWalletEncryptedBundle ); } @@ -520,7 +520,7 @@ export class IframeStamper { async extractKeyEncryptedBundle(keyFormat?: KeyFormat): Promise { return this.createRequest( IframeEventType.ExtractKeyEncryptedBundle, - { keyFormat }, + { keyFormat } ); } @@ -545,7 +545,7 @@ export class IframeStamper { async stamp(payload: string): Promise { if (this.iframePublicKey === null) { throw new Error( - "null iframe public key. Have you called/awaited .init()?", + "null iframe public key. Have you called/awaited .init()?" ); } @@ -558,11 +558,16 @@ export class IframeStamper { * Function to sign a message using an embedded private key in-memory within an iframe * Returns the signed message string * @param {TSignableMessage} message - The message to sign with type (Ethereum or Solana) + * @param {string} address - [Optional] Address to sign with * @returns {Promise} The signed message string */ - async signMessage(message: TSignableMessage): Promise { + async signMessage( + message: TSignableMessage, + address: string + ): Promise { return this.createRequest(IframeEventType.SignMessage, { value: JSON.stringify(message), + address, }); } @@ -570,11 +575,16 @@ export class IframeStamper { * Function to sign a transaction using an embedded private key in-memory within an iframe * Returns the signed, serialized transaction payload * @param {TSignableTransaction} transaction - The transaction to sign with type (Ethereum or Solana) + * @param {string} address - [Optional] Address to sign with * @returns {Promise} The signed, serialized transaction payload */ - async signTransaction(transaction: TSignableTransaction): Promise { + async signTransaction( + transaction: TSignableTransaction, + address?: string + ): Promise { return this.createRequest(IframeEventType.SignTransaction, { value: JSON.stringify(transaction), + address, }); } From 2df00d8860fd7ad629adce8f92c22e1eeccba4f6 Mon Sep 17 00:00:00 2001 From: Andrew Min Date: Mon, 24 Nov 2025 22:25:17 -0500 Subject: [PATCH 3/9] iterating --- .../src/components/Export.tsx | 14 ------- .../src/components/ExportWalletAccount.tsx | 17 ++++++++- examples/with-solana/src/index.ts | 30 ++++++++++++--- packages/iframe-stamper/src/index.ts | 38 +++++++++---------- 4 files changed, 59 insertions(+), 40 deletions(-) diff --git a/examples/wallet-export-sign/src/components/Export.tsx b/examples/wallet-export-sign/src/components/Export.tsx index 0e865b8ba..93df01926 100644 --- a/examples/wallet-export-sign/src/components/Export.tsx +++ b/examples/wallet-export-sign/src/components/Export.tsx @@ -146,7 +146,6 @@ export function Export(props: ExportProps) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [props.setIframeStamper, iframeStamper]); - // For the sake of demonstration, only allow signing one message at a time (embedded private key will be cleared after signature) const signMessage = () => { if (iframeStamper === null) { alert("Iframe not ready — reveal private key first."); @@ -162,15 +161,8 @@ export function Export(props: ExportProps) { console.error("Error signing message:", error); alert("Error signing message: " + error.message); }); - - // now clear the embedded private key from iframe's memory - iframeStamper.clearEmbeddedPrivateKey().catch((error: Error) => { - console.error("Error clearing embedded private key:", error); - alert("Error clearing embedded private key: " + error.message); - }); }; - // For the sake of demonstration, only allow signing one message at a time (embedded private key will be cleared after signature) const signTransaction = () => { if (iframeStamper === null) { alert("Iframe not ready — reveal private key first."); @@ -194,12 +186,6 @@ export function Export(props: ExportProps) { console.error("Error signing transaction:", error); alert("Error signing transaction: " + error.message); }); - - // now clear the embedded private key from iframe's memory - iframeStamper.clearEmbeddedPrivateKey().catch((error: Error) => { - console.error("Error clearing embedded private key:", error); - alert("Error clearing embedded private key: " + error.message); - }); }; const copyToClipboard = async (text: string) => { diff --git a/examples/wallet-export-sign/src/components/ExportWalletAccount.tsx b/examples/wallet-export-sign/src/components/ExportWalletAccount.tsx index d024341bc..8dea3fec9 100644 --- a/examples/wallet-export-sign/src/components/ExportWalletAccount.tsx +++ b/examples/wallet-export-sign/src/components/ExportWalletAccount.tsx @@ -26,14 +26,29 @@ export function ExportWalletAccount(props: ExportWalletAccountProps) { // Export the selected wallet account and set the iframe to be visible const exportWalletAccount = async () => { + console.log("ayo"); if (iframeStamper === null) { alert("Cannot export wallet account without an iframe."); return; } + console.log("after"); + + // Check to ensure there's an embedded key + let embeddedKey = await iframeStamper.getEmbeddedPublicKey(); + + console.log("embedded key", embeddedKey); + + if (!embeddedKey) { + console.log( + "Iframe not ready — embedded key not found. Creating a new one...", + ); + + embeddedKey = await iframeStamper.initEmbeddedKey(); + } const response = await axios.post("/api/exportWalletAccount", { walletAccountAddress: props.walletAccountAddress, - targetPublicKey: iframeStamper.publicKey(), + targetPublicKey: embeddedKey, }); let injected = await iframeStamper.injectKeyExportBundle( diff --git a/examples/with-solana/src/index.ts b/examples/with-solana/src/index.ts index 54742051f..670422a76 100644 --- a/examples/with-solana/src/index.ts +++ b/examples/with-solana/src/index.ts @@ -76,7 +76,7 @@ async function main() { `- Any online faucet (e.g. https://faucet.solana.com/)`, `\nTo check your balance: https://explorer.solana.com/address/${solAddress}?cluster=${network}`, `\n--------`, - ].join("\n"), + ].join("\n") ); // Await user confirmation to continue await prompts([ @@ -120,14 +120,14 @@ async function main() { const { r, s } = getSignatureFromActivity(activity); return Buffer.from(`${r}${s}`, "hex"); - }, + } ); } const isValidSignature = nacl.sign.detached.verify( messageAsUint8Array, signature, - bs58.decode(solAddress), + bs58.decode(solAddress) ); if (!isValidSignature) { @@ -179,11 +179,21 @@ async function main() { connection, }); + console.log( + "unsigned transaction", + Buffer.from( + transaction.serialize({ + requireAllSignatures: false, + verifySignatures: false, + }) + ).toString("hex") + ); + let signedTransaction: Transaction | undefined = undefined; // legacy try { signedTransaction = (await turnkeySigner.signTransaction( transaction, - solAddress, + solAddress )) as Transaction; } catch (error: any) { await handleActivityError(turnkeyClient, error).then( @@ -194,10 +204,10 @@ async function main() { const decodedTransaction = Buffer.from( getSignedTransactionFromActivity(activity), - "hex", + "hex" ); signedTransaction = Transaction.from(decodedTransaction); - }, + } ); } @@ -207,6 +217,14 @@ async function main() { throw new Error("unable to verify transaction signatures"); } + // return; + signedTransaction = Transaction.from( + Buffer.from( + "01fb953ea842e31b260dd118a018587cb096135f24c4de39bf8db74ae85a8a84d5a6828cc745901c2a904eaaa756a445729a009de8f0719b4575590a04a360670601000103e05271368f77a2c5fefe77ce50e2b2f93ceb671eee8b172734c8d4df9d9eddc10d42099a5e0aaeaad1d4ede263662787cb3f6291a6ede340c4aa7ca26249dbe3000000000000000000000000000000000000000000000000000000000000000062264c8f48f4d046e84fd3b8fb907bf7a850da1cdb151013d8f17f25a1eae5a901020200010c020000006400000000000000", + "hex" + ) + ); + // 3. Broadcast the signed payload await solanaNetwork.broadcast(connection, signedTransaction!); diff --git a/packages/iframe-stamper/src/index.ts b/packages/iframe-stamper/src/index.ts index 79a45a9e6..4b70f3ffc 100644 --- a/packages/iframe-stamper/src/index.ts +++ b/packages/iframe-stamper/src/index.ts @@ -60,7 +60,7 @@ export enum IframeEventType { // Value: MessageChannel port TurnkeyInitMessageChannel = "TURNKEY_INIT_MESSAGE_CHANNEL", // Event sent by the parent to get the iframe target embedded key's public key. - // Value: none + // Value: the iframe public key GetEmbeddedPublicKey = "GET_EMBEDDED_PUBLIC_KEY", // Event sent by the parent to clear the iframe's embedded key. // Value: none @@ -191,7 +191,7 @@ export class IframeStamper { if (typeof MessageChannel === "undefined") { throw new Error( - "Cannot initialize iframe without MessageChannel support" + "Cannot initialize iframe without MessageChannel support", ); } @@ -202,7 +202,7 @@ export class IframeStamper { if (this.container.querySelector(`#${config.iframeElementId}`)) { throw new Error( - `Iframe element with ID ${config.iframeElementId} already exists` + `Iframe element with ID ${config.iframeElementId} already exists`, ); } @@ -288,7 +288,7 @@ export class IframeStamper { * @throws {Error} When contentWindow or contentWindow.postMessage does not exist */ async init( - dangerouslyOverrideIframeKeyTtl?: number | undefined + dangerouslyOverrideIframeKeyTtl?: number | undefined, ): Promise { return new Promise((resolve, reject) => { this.container.appendChild(this.iframe); @@ -297,8 +297,8 @@ export class IframeStamper { if (!this.iframe.contentWindow?.postMessage) { reject( new Error( - "contentWindow or contentWindow.postMessage does not exist" - ) + "contentWindow or contentWindow.postMessage does not exist", + ), ); return; } @@ -309,7 +309,7 @@ export class IframeStamper { dangerouslyOverrideIframeKeyTtl: dangerouslyOverrideIframeKeyTtl, }, this.iframeOrigin, - [this.messageChannel.port2] + [this.messageChannel.port2], ); }); @@ -352,7 +352,7 @@ export class IframeStamper { */ async getEmbeddedPublicKey(): Promise { const publicKey = await this.createRequest( - IframeEventType.GetEmbeddedPublicKey + IframeEventType.GetEmbeddedPublicKey, ); this.iframePublicKey = publicKey; @@ -378,7 +378,7 @@ export class IframeStamper { */ async initEmbeddedKey(): Promise { const publicKey = await this.createRequest( - IframeEventType.InitEmbeddedKey + IframeEventType.InitEmbeddedKey, ); this.iframePublicKey = publicKey; @@ -394,7 +394,7 @@ export class IframeStamper { */ private createRequest( type: IframeEventType, - payload: any = {} + payload: any = {}, ): Promise { return new Promise((resolve, reject) => { const requestId = generateUUID(); @@ -443,7 +443,7 @@ export class IframeStamper { bundle: string, organizationId: string, keyFormat?: KeyFormat, - address?: string + address?: string, ): Promise { return this.createRequest(IframeEventType.InjectKeyExportBundle, { value: bundle, @@ -464,14 +464,14 @@ export class IframeStamper { */ async injectWalletExportBundle( bundle: string, - organizationId: string + organizationId: string, ): Promise { return this.createRequest( IframeEventType.InjectWalletExportBundle, { value: bundle, organizationId, - } + }, ); } @@ -486,7 +486,7 @@ export class IframeStamper { async injectImportBundle( bundle: string, organizationId: string, - userId: string + userId: string, ): Promise { return this.createRequest(IframeEventType.InjectImportBundle, { value: bundle, @@ -504,7 +504,7 @@ export class IframeStamper { */ async extractWalletEncryptedBundle(): Promise { return this.createRequest( - IframeEventType.ExtractWalletEncryptedBundle + IframeEventType.ExtractWalletEncryptedBundle, ); } @@ -520,7 +520,7 @@ export class IframeStamper { async extractKeyEncryptedBundle(keyFormat?: KeyFormat): Promise { return this.createRequest( IframeEventType.ExtractKeyEncryptedBundle, - { keyFormat } + { keyFormat }, ); } @@ -545,7 +545,7 @@ export class IframeStamper { async stamp(payload: string): Promise { if (this.iframePublicKey === null) { throw new Error( - "null iframe public key. Have you called/awaited .init()?" + "null iframe public key. Have you called/awaited .init()?", ); } @@ -563,7 +563,7 @@ export class IframeStamper { */ async signMessage( message: TSignableMessage, - address: string + address: string, ): Promise { return this.createRequest(IframeEventType.SignMessage, { value: JSON.stringify(message), @@ -580,7 +580,7 @@ export class IframeStamper { */ async signTransaction( transaction: TSignableTransaction, - address?: string + address?: string, ): Promise { return this.createRequest(IframeEventType.SignTransaction, { value: JSON.stringify(transaction), From 26f6e65c2df7b20ff22e72b04831188cc2ecd98a Mon Sep 17 00:00:00 2001 From: Andrew Min Date: Mon, 24 Nov 2025 22:37:04 -0500 Subject: [PATCH 4/9] cleanup --- .../src/components/Export.tsx | 54 ++++++++++++------- .../src/components/ExportWalletAccount.tsx | 6 +-- examples/with-solana/src/index.ts | 30 +++-------- packages/iframe-stamper/src/index.ts | 2 +- 4 files changed, 42 insertions(+), 50 deletions(-) diff --git a/examples/wallet-export-sign/src/components/Export.tsx b/examples/wallet-export-sign/src/components/Export.tsx index 93df01926..41e6bc6f2 100644 --- a/examples/wallet-export-sign/src/components/Export.tsx +++ b/examples/wallet-export-sign/src/components/Export.tsx @@ -97,7 +97,7 @@ export function Export(props: ExportProps) { const [txSerialized, setTxSerialized] = useState(""); const [txSigned, setTxSigned] = useState(""); - const [initializing, setInitializing] = useState(false); + const [_initializing, setInitializing] = useState(false); useEffect(() => { setIframeDisplay(props.iframeDisplay); @@ -146,46 +146,60 @@ export function Export(props: ExportProps) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [props.setIframeStamper, iframeStamper]); - const signMessage = () => { + const signMessage = async () => { if (iframeStamper === null) { alert("Iframe not ready — reveal private key first."); return; } - iframeStamper - .signMessage({ message, type: MessageType.Solana }) - .then((sig: string) => { - setSignature(sig); - }) - .catch((error: Error) => { - console.error("Error signing message:", error); - alert("Error signing message: " + error.message); + // Check to ensure there's an embedded key + const existingKey = await iframeStamper.getEmbeddedPublicKey(); + if (!existingKey) { + alert("Iframe not ready — embedded key not found."); + return; + } + + try { + const signedMessage = await iframeStamper.signMessage({ + message, + type: MessageType.Solana, }); + setSignature(signedMessage); + } catch (error: any) { + console.error("Error signing message:", error); + alert("Error signing message: " + error.message); + } }; - const signTransaction = () => { + const signTransaction = async () => { if (iframeStamper === null) { alert("Iframe not ready — reveal private key first."); return; } + // Check to ensure there's an embedded key + const existingKey = await iframeStamper.getEmbeddedPublicKey(); + if (!existingKey) { + alert("Iframe not ready — embedded key not found."); + return; + } + if (!txSerialized || txSerialized.trim() === "") { alert("Please provide a Solana transaction."); return; } - iframeStamper - .signTransaction({ + try { + const signedTransaction = await iframeStamper.signTransaction({ transaction: txSerialized, type: TransactionType.Solana, - }) - .then((signed: string) => { - setTxSigned(signed); - }) - .catch((error: Error) => { - console.error("Error signing transaction:", error); - alert("Error signing transaction: " + error.message); }); + + setTxSigned(signedTransaction); + } catch (error: any) { + console.error("Error signing transaction:", error); + alert("Error signing transaction: " + error.message); + } }; const copyToClipboard = async (text: string) => { diff --git a/examples/wallet-export-sign/src/components/ExportWalletAccount.tsx b/examples/wallet-export-sign/src/components/ExportWalletAccount.tsx index 8dea3fec9..8eed0b89d 100644 --- a/examples/wallet-export-sign/src/components/ExportWalletAccount.tsx +++ b/examples/wallet-export-sign/src/components/ExportWalletAccount.tsx @@ -26,20 +26,16 @@ export function ExportWalletAccount(props: ExportWalletAccountProps) { // Export the selected wallet account and set the iframe to be visible const exportWalletAccount = async () => { - console.log("ayo"); if (iframeStamper === null) { alert("Cannot export wallet account without an iframe."); return; } - console.log("after"); // Check to ensure there's an embedded key let embeddedKey = await iframeStamper.getEmbeddedPublicKey(); - console.log("embedded key", embeddedKey); - if (!embeddedKey) { - console.log( + console.warn( "Iframe not ready — embedded key not found. Creating a new one...", ); diff --git a/examples/with-solana/src/index.ts b/examples/with-solana/src/index.ts index 670422a76..54742051f 100644 --- a/examples/with-solana/src/index.ts +++ b/examples/with-solana/src/index.ts @@ -76,7 +76,7 @@ async function main() { `- Any online faucet (e.g. https://faucet.solana.com/)`, `\nTo check your balance: https://explorer.solana.com/address/${solAddress}?cluster=${network}`, `\n--------`, - ].join("\n") + ].join("\n"), ); // Await user confirmation to continue await prompts([ @@ -120,14 +120,14 @@ async function main() { const { r, s } = getSignatureFromActivity(activity); return Buffer.from(`${r}${s}`, "hex"); - } + }, ); } const isValidSignature = nacl.sign.detached.verify( messageAsUint8Array, signature, - bs58.decode(solAddress) + bs58.decode(solAddress), ); if (!isValidSignature) { @@ -179,21 +179,11 @@ async function main() { connection, }); - console.log( - "unsigned transaction", - Buffer.from( - transaction.serialize({ - requireAllSignatures: false, - verifySignatures: false, - }) - ).toString("hex") - ); - let signedTransaction: Transaction | undefined = undefined; // legacy try { signedTransaction = (await turnkeySigner.signTransaction( transaction, - solAddress + solAddress, )) as Transaction; } catch (error: any) { await handleActivityError(turnkeyClient, error).then( @@ -204,10 +194,10 @@ async function main() { const decodedTransaction = Buffer.from( getSignedTransactionFromActivity(activity), - "hex" + "hex", ); signedTransaction = Transaction.from(decodedTransaction); - } + }, ); } @@ -217,14 +207,6 @@ async function main() { throw new Error("unable to verify transaction signatures"); } - // return; - signedTransaction = Transaction.from( - Buffer.from( - "01fb953ea842e31b260dd118a018587cb096135f24c4de39bf8db74ae85a8a84d5a6828cc745901c2a904eaaa756a445729a009de8f0719b4575590a04a360670601000103e05271368f77a2c5fefe77ce50e2b2f93ceb671eee8b172734c8d4df9d9eddc10d42099a5e0aaeaad1d4ede263662787cb3f6291a6ede340c4aa7ca26249dbe3000000000000000000000000000000000000000000000000000000000000000062264c8f48f4d046e84fd3b8fb907bf7a850da1cdb151013d8f17f25a1eae5a901020200010c020000006400000000000000", - "hex" - ) - ); - // 3. Broadcast the signed payload await solanaNetwork.broadcast(connection, signedTransaction!); diff --git a/packages/iframe-stamper/src/index.ts b/packages/iframe-stamper/src/index.ts index 4b70f3ffc..596927909 100644 --- a/packages/iframe-stamper/src/index.ts +++ b/packages/iframe-stamper/src/index.ts @@ -563,7 +563,7 @@ export class IframeStamper { */ async signMessage( message: TSignableMessage, - address: string, + address?: string, ): Promise { return this.createRequest(IframeEventType.SignMessage, { value: JSON.stringify(message), From 0751f676da62d551828023f9a7b05ed0dc2fee0e Mon Sep 17 00:00:00 2001 From: Andrew Min Date: Mon, 24 Nov 2025 23:03:04 -0500 Subject: [PATCH 5/9] specify signing address --- .../src/components/Export.tsx | 23 ++++++++++++------- .../src/components/ExportWalletAccount.tsx | 2 ++ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/examples/wallet-export-sign/src/components/Export.tsx b/examples/wallet-export-sign/src/components/Export.tsx index 41e6bc6f2..7f69575bc 100644 --- a/examples/wallet-export-sign/src/components/Export.tsx +++ b/examples/wallet-export-sign/src/components/Export.tsx @@ -13,6 +13,7 @@ interface ExportProps { iframeDisplay: string; setIframeStamper: Dispatch>; showSigning?: boolean; // Only show signing UI for wallet accounts, not wallets + walletAccountAddress?: string; // Address of the wallet account being exported } const containerStyles: React.CSSProperties = { @@ -160,10 +161,13 @@ export function Export(props: ExportProps) { } try { - const signedMessage = await iframeStamper.signMessage({ - message, - type: MessageType.Solana, - }); + const signedMessage = await iframeStamper.signMessage( + { + message, + type: MessageType.Solana, + }, + props.walletAccountAddress, + ); setSignature(signedMessage); } catch (error: any) { console.error("Error signing message:", error); @@ -190,10 +194,13 @@ export function Export(props: ExportProps) { } try { - const signedTransaction = await iframeStamper.signTransaction({ - transaction: txSerialized, - type: TransactionType.Solana, - }); + const signedTransaction = await iframeStamper.signTransaction( + { + transaction: txSerialized, + type: TransactionType.Solana, + }, + props.walletAccountAddress, + ); setTxSigned(signedTransaction); } catch (error: any) { diff --git a/examples/wallet-export-sign/src/components/ExportWalletAccount.tsx b/examples/wallet-export-sign/src/components/ExportWalletAccount.tsx index 8eed0b89d..f267e2024 100644 --- a/examples/wallet-export-sign/src/components/ExportWalletAccount.tsx +++ b/examples/wallet-export-sign/src/components/ExportWalletAccount.tsx @@ -51,6 +51,7 @@ export function ExportWalletAccount(props: ExportWalletAccountProps) { response.data["exportBundle"], props.organizationId, KeyFormat.Hexadecimal, + props.walletAccountAddress, ); if (injected !== true) { alert("Unexpected error while injecting export bundle."); @@ -121,6 +122,7 @@ export function ExportWalletAccount(props: ExportWalletAccountProps) { iframeUrl={process.env.NEXT_PUBLIC_EXPORT_SIGN_IFRAME_URL!} turnkeyBaseUrl={process.env.NEXT_PUBLIC_BASE_URL!} showSigning={props.addressFormat === "ADDRESS_FORMAT_SOLANA"} + walletAccountAddress={props.walletAccountAddress} /> From 08fd1f5bd83e9ac21b4848ee3ca65e4022e17acb Mon Sep 17 00:00:00 2001 From: Andrew Min Date: Mon, 24 Nov 2025 23:05:22 -0500 Subject: [PATCH 6/9] add broadcasting-specific script --- examples/with-solana/package.json | 1 + examples/with-solana/src/broadcast.ts | 121 ++++++++++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 examples/with-solana/src/broadcast.ts diff --git a/examples/with-solana/package.json b/examples/with-solana/package.json index dc11889f5..b5300b9f1 100644 --- a/examples/with-solana/package.json +++ b/examples/with-solana/package.json @@ -11,6 +11,7 @@ "jupiter-swap": "pnpm tsx src/jupiterSwap.ts", "with-fee-payer": "pnpm tsx src/withFeePayer.ts", "add-signature": "pnpm tsx src/addSignature.ts", + "broadcast": "pnpm tsx src/broadcast.ts", "clean": "rimraf ./dist ./.cache", "typecheck": "tsc --noEmit" }, diff --git a/examples/with-solana/src/broadcast.ts b/examples/with-solana/src/broadcast.ts new file mode 100644 index 000000000..777dc85c2 --- /dev/null +++ b/examples/with-solana/src/broadcast.ts @@ -0,0 +1,121 @@ +import * as dotenv from "dotenv"; +import * as path from "path"; +import prompts from "prompts"; +import { Transaction, VersionedTransaction } from "@solana/web3.js"; + +// Load environment variables from `.env.local` +dotenv.config({ path: path.resolve(process.cwd(), ".env.local") }); + +import { solanaNetwork } from "./utils"; + +async function main() { + // Create a node connection; if no env var is found, default to public devnet RPC + const nodeEndpoint = + process.env.SOLANA_NODE || "https://api.devnet.solana.com"; + const connection = solanaNetwork.connect(nodeEndpoint); + + console.log(`\nConnected to Solana network: ${nodeEndpoint}\n`); + + // Prompt for the signed transaction payload + const { payload } = await prompts([ + { + type: "text", + name: "payload", + message: "Enter the signed transaction payload (hex or base64):", + validate: (value) => + value && value.trim().length > 0 + ? true + : "Payload cannot be empty", + }, + ]); + + if (!payload) { + console.log("No payload provided. Exiting."); + process.exit(0); + } + + let signedTransaction: Transaction | VersionedTransaction; + + try { + // Try to decode as hex first + const buffer = Buffer.from(payload.trim(), "hex"); + + // Try to deserialize as VersionedTransaction first + try { + signedTransaction = VersionedTransaction.deserialize(buffer); + console.log("\nDetected versioned transaction"); + } catch { + // Fall back to legacy Transaction + signedTransaction = Transaction.from(buffer); + console.log("\nDetected legacy transaction"); + } + } catch (hexError) { + try { + // Try base64 if hex fails + const buffer = Buffer.from(payload.trim(), "base64"); + + try { + signedTransaction = VersionedTransaction.deserialize(buffer); + console.log("\nDetected versioned transaction (base64)"); + } catch { + signedTransaction = Transaction.from(buffer); + console.log("\nDetected legacy transaction (base64)"); + } + } catch (base64Error) { + console.error("\nFailed to decode transaction payload."); + console.error("Please provide a valid hex or base64 encoded transaction."); + process.exit(1); + } + } + + // Verify signatures for legacy transactions + if ("verifySignatures" in signedTransaction) { + const verified = signedTransaction.verifySignatures(); + if (!verified) { + console.warn( + "\n⚠️ Warning: Transaction signatures could not be verified", + ); + const { proceed } = await prompts([ + { + type: "confirm", + name: "proceed", + message: "Do you want to proceed anyway?", + initial: false, + }, + ]); + + if (!proceed) { + console.log("Broadcast cancelled."); + process.exit(0); + } + } else { + console.log("✓ Transaction signatures verified"); + } + } + + // Confirm before broadcasting + const { confirm } = await prompts([ + { + type: "confirm", + name: "confirm", + message: "Ready to broadcast this transaction?", + initial: true, + }, + ]); + + if (!confirm) { + console.log("Broadcast cancelled."); + process.exit(0); + } + + // Broadcast the signed transaction + console.log("\nBroadcasting transaction..."); + await solanaNetwork.broadcast(connection, signedTransaction); + + process.exit(0); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); From 7ac558c39c3fa0ddeb6e695182a49f03ee6d4f00 Mon Sep 17 00:00:00 2001 From: Andrew Min Date: Mon, 24 Nov 2025 23:15:42 -0500 Subject: [PATCH 7/9] add changeset --- .changeset/true-actors-grow.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/true-actors-grow.md diff --git a/.changeset/true-actors-grow.md b/.changeset/true-actors-grow.md new file mode 100644 index 000000000..b6c43315e --- /dev/null +++ b/.changeset/true-actors-grow.md @@ -0,0 +1,5 @@ +--- +"@turnkey/iframe-stamper": minor +--- + +Add optional address parameter for methods intended to be used within the export-and-sign iframe. Also improves documentation (TypeDocs) From ca771bd89eb6ee9bb16038c9fc95a38e2c65cc9b Mon Sep 17 00:00:00 2001 From: Andrew Min Date: Mon, 24 Nov 2025 23:50:24 -0500 Subject: [PATCH 8/9] minor redesign --- .../src/components/Export.module.css | 269 ++++++++++++++++++ .../src/components/Export.tsx | 250 ++++++---------- .../src/components/Modal.tsx | 32 ++- .../src/pages/index.module.css | 253 +++++++++++----- .../src/pages/wallet/[id].tsx | 4 - examples/with-solana/src/broadcast.ts | 8 +- 6 files changed, 556 insertions(+), 260 deletions(-) create mode 100644 examples/wallet-export-sign/src/components/Export.module.css diff --git a/examples/wallet-export-sign/src/components/Export.module.css b/examples/wallet-export-sign/src/components/Export.module.css new file mode 100644 index 000000000..4b9c19602 --- /dev/null +++ b/examples/wallet-export-sign/src/components/Export.module.css @@ -0,0 +1,269 @@ +.container { + margin-top: 32px; + display: flex; + flex-direction: column; + gap: 28px; + width: 100%; + max-width: 100%; + font-family: + Inter, + system-ui, + -apple-system, + "Segoe UI", + Roboto, + "Helvetica Neue", + Arial, + sans-serif; + padding: 0; + box-sizing: border-box; +} + +.card { + padding: 0; + border-radius: 12px; + border: 1px solid #e5e7eb; + background: #ffffff; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); + transition: all 0.2s ease; + width: 100%; + box-sizing: border-box; + overflow: hidden; +} + +.card:hover { + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.06); + border-color: #d1d5db; +} + +.cardHeader { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 24px; + background: linear-gradient(to bottom, #fafbfc, #f8f9fa); + border-bottom: 1px solid #e5e7eb; + gap: 16px; +} + +.cardTitle { + font-size: 15px; + font-weight: 600; + color: #111827; + margin: 0; + letter-spacing: -0.01em; +} + +.cardSubtitle { + font-size: 12px; + color: #6b7280; + font-weight: 500; + white-space: nowrap; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.cardBody { + padding: 20px; +} + +.textarea { + width: 100%; + min-height: 110px; + padding: 14px 16px; + font-family: + ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", + monospace; + font-size: 14px; + line-height: 1.6; + border: 1px solid #d1d5db; + border-radius: 8px; + resize: none; + transition: + border-color 0.2s ease, + box-shadow 0.2s ease, + background-color 0.2s ease; + background: #fafafa; + box-sizing: border-box; +} + +.textarea:focus { + outline: none; + border-color: #2563eb; + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.08); + background: #ffffff; +} + +.textarea::placeholder { + color: #9ca3af; +} + +.buttonGroup { + display: flex; + gap: 12px; + margin-top: 16px; + flex-wrap: wrap; +} + +.button { + padding: 11px 20px; + font-size: 14px; + font-weight: 500; + border-radius: 8px; + border: 1px solid #d1d5db; + background: #ffffff; + color: #374151; + cursor: pointer; + transition: all 0.15s ease; + font-family: Inter, system-ui, sans-serif; + letter-spacing: -0.01em; +} + +.button:hover:not(:disabled) { + background: #f9fafb; + border-color: #9ca3af; +} + +.button:active:not(:disabled) { + transform: translateY(1px); +} + +.button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.buttonPrimary { + background: #050a0b; + color: #ffffff; + border-color: #050a0b; +} + +.buttonPrimary:hover:not(:disabled) { + background: #1a1f23; + border-color: #1a1f23; +} + +.outputSection { + margin-top: 24px; + padding: 20px; + background: #f9fafb; + border-radius: 8px; + border: 1px solid #e5e7eb; +} + +.outputLabel { + font-size: 12px; + font-weight: 600; + color: #6b7280; + margin-bottom: 12px; + letter-spacing: 0.03em; + text-transform: uppercase; +} + +.monoBox { + font-family: + ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", + monospace; + font-size: 12px; + padding: 14px 16px; + border-radius: 6px; + background: #ffffff; + border: 1px solid #e5e7eb; + word-break: break-all; + white-space: pre-wrap; + color: #1f2937; + line-height: 1.7; + max-height: 140px; + overflow-y: auto; + box-sizing: border-box; +} + +.monoBox::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +.monoBox::-webkit-scrollbar-track { + background: #f1f3f5; + border-radius: 4px; +} + +.monoBox::-webkit-scrollbar-thumb { + background: #cbd5e0; + border-radius: 4px; +} + +.monoBox::-webkit-scrollbar-thumb:hover { + background: #a0aec0; +} + +.copyButton { + padding: 8px 14px; + font-size: 12px; + font-weight: 500; + border-radius: 6px; + border: 1px solid #d1d5db; + background: #ffffff; + color: #374151; + cursor: pointer; + transition: all 0.15s ease; + margin-top: 12px; + letter-spacing: -0.01em; +} + +.copyButton:hover { + background: #f9fafb; + border-color: #9ca3af; +} + +.copyButton:active { + transform: translateY(1px); +} + +.iframeContainer { + margin-bottom: 20px; +} + +@media (min-width: 640px) { + .cardBody { + padding: 24px; + } + + .textarea { + min-height: 120px; + } +} + +@media (min-width: 768px) { + .cardHeader { + padding: 24px 28px; + } + + .cardBody { + padding: 28px; + } + + .cardTitle { + font-size: 16px; + } + + .textarea { + font-size: 14px; + padding: 16px 18px; + min-height: 130px; + } + + .container { + gap: 32px; + } +} + +@media (min-width: 1024px) { + .cardHeader { + padding: 24px 32px; + } + + .cardBody { + padding: 32px; + } +} diff --git a/examples/wallet-export-sign/src/components/Export.tsx b/examples/wallet-export-sign/src/components/Export.tsx index 7f69575bc..a07c27a2f 100644 --- a/examples/wallet-export-sign/src/components/Export.tsx +++ b/examples/wallet-export-sign/src/components/Export.tsx @@ -6,6 +6,7 @@ import { MessageType, } from "@turnkey/iframe-stamper"; import { Dispatch, SetStateAction, useEffect, useState } from "react"; +import styles from "./Export.module.css"; interface ExportProps { iframeUrl: string; @@ -16,48 +17,6 @@ interface ExportProps { walletAccountAddress?: string; // Address of the wallet account being exported } -const containerStyles: React.CSSProperties = { - marginTop: 16, - display: "flex", - flexDirection: "column", - gap: 16, - width: "100%", - maxWidth: "100%", - fontFamily: - "Inter, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial", - maxHeight: "min(70vh, 600px)", - overflowY: "auto", - overflowX: "hidden", - padding: "0", - boxSizing: "border-box", -}; - -const cardStyles: React.CSSProperties = { - padding: 16, - borderRadius: 12, - border: "1px solid rgba(216,219,227,1)", - background: "#ffffff", - boxShadow: "0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04)", - transition: "box-shadow 0.2s ease", - width: "100%", - boxSizing: "border-box", - overflowX: "hidden", -}; - -const monoBox: React.CSSProperties = { - fontFamily: "monospace", - fontSize: 13, - padding: 12, - borderRadius: 8, - background: "#f8f9fa", - border: "1px solid #e0e3e7", - wordBreak: "break-word", - whiteSpace: "pre-wrap", - color: "#2b2f33", - maxHeight: "200px", - overflowY: "auto", -}; - const iframeCss = ` iframe { box-sizing: border-box; @@ -67,16 +26,16 @@ const iframeCss = ` border-radius: 12px; border-width: 1px; border-style: solid; - border-color: rgba(216, 219, 227, 1); - padding: 16px; + border-color: #e5e7eb; + padding: 20px; background: white; - box-shadow: 0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04); - margin-bottom: 8px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); + margin-bottom: 0; } @media (min-width: 768px) { iframe { - padding: 20px; + padding: 24px; } } `; @@ -223,145 +182,116 @@ export function Export(props: ExportProps) { return (
{/* keep iframe mounted but visually controlled by parent */} -
+
{iframeDisplay === "block" && props.showSigning ? ( -
+
{/* Message signing */} -
-
- Sign arbitrary message - ed25519 signature +
+
+

Sign arbitrary message

+ ed25519
-