Skip to main content
Glama
turnkey-stacks-example.tsโ€ข14.3 kB
/** * Complete TurnkeyHQ + Stacks Integration Example * * This file demonstrates how to integrate TurnkeyHQ's secure enclave technology * with your SIP-compliant Stacks smart contract to create a seamless purchase * experience without browser extensions. */ import { TurnkeySDK } from '@turnkey/sdk-server'; import { TurnkeyClient } from '@turnkey/sdk-client'; import { makeContractCall, makeSTXTokenTransfer, broadcastTransaction, StacksNetwork, StacksTestnet, StacksMainnet, standardPrincipalCV, uintCV, noneCV, PostConditionMode, FungibleConditionCode, makeStandardFungiblePostCondition, createAssetInfo } from '@stacks/transactions'; // Configuration const CONFIG = { TURNKEY_API_PRIVATE_KEY: process.env.TURNKEY_PRIVATE_KEY!, TURNKEY_API_PUBLIC_KEY: process.env.TURNKEY_PUBLIC_KEY!, STACKS_CONTRACT_ADDRESS: 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE', STACKS_CONTRACT_NAME: 'embedded-wallet-v4', STACKS_NETWORK: process.env.STACKS_NETWORK === 'mainnet' ? new StacksMainnet() : new StacksTestnet(), }; /** * TurnkeyHQ + Stacks Integration Class * * This class handles the complete workflow of creating secure wallets, * registering users on the Stacks contract, and executing transactions * without requiring browser extensions. */ export class TurnkeyStacksIntegration { private sdk: TurnkeySDK; private client: TurnkeyClient; private network: StacksNetwork; constructor() { this.sdk = new TurnkeySDK({ apiPrivateKey: CONFIG.TURNKEY_API_PRIVATE_KEY, apiPublicKey: CONFIG.TURNKEY_API_PUBLIC_KEY, baseUrl: 'https://api.turnkey.com', }); this.client = new TurnkeyClient({ apiPrivateKey: CONFIG.TURNKEY_API_PRIVATE_KEY, apiPublicKey: CONFIG.TURNKEY_API_PUBLIC_KEY, baseUrl: 'https://api.turnkey.com', }); this.network = CONFIG.STACKS_NETWORK; } /** * Step 1: Create a secure user wallet with TurnkeyHQ * * This creates a sub-organization for the user and generates * a Stacks-compatible private key in a secure enclave. */ async createUserWallet(userId: string, userEmail: string) { console.log(`Creating secure wallet for user: ${userId}`); try { // Create sub-organization for user isolation const subOrg = await this.sdk.createSubOrganization({ subOrganizationName: `user-${userId}`, rootQuorumThreshold: 1, rootUsers: [ { userName: userId, userEmail: userEmail, }, ], }); console.log(`Created sub-organization: ${subOrg.subOrganizationId}`); // Create wallet within the sub-organization const wallet = await this.sdk.createWallet({ organizationId: subOrg.subOrganizationId, walletName: 'stacks-main', }); console.log(`Created wallet: ${wallet.walletId}`); // Create Stacks-compatible private key const privateKey = await this.sdk.createPrivateKey({ organizationId: subOrg.subOrganizationId, privateKeyName: 'stacks-key', curve: 'CURVE_SECP256K1', addressFormats: ['ADDRESS_FORMAT_STACKS_P2PKH'], privateKeyTags: ['stacks', 'main'], }); console.log(`Created private key: ${privateKey.privateKeyId}`); return { subOrganizationId: subOrg.subOrganizationId, walletId: wallet.walletId, privateKeyId: privateKey.privateKeyId, address: privateKey.addresses[0].address, publicKey: privateKey.publicKey, }; } catch (error) { console.error('Failed to create user wallet:', error); throw error; } } /** * Step 2: Register user on the SIP-compliant Stacks contract * * This calls the register-user-secure function on your contract * with proof-of-possession to establish the user's identity. */ async registerUserOnContract( userAddress: string, publicKey: string, subOrganizationId: string, privateKeyId: string ) { console.log(`Registering user ${userAddress} on Stacks contract`); try { // Create challenge message for proof-of-possession const challengeMessage = Buffer.from(userAddress, 'utf8'); // Sign the challenge with TurnkeyHQ to prove key possession const signature = await this.sdk.signRawPayload({ organizationId: subOrganizationId, privateKeyId: privateKeyId, payload: challengeMessage.toString('hex'), encoding: 'PAYLOAD_ENCODING_HEXADECIMAL', }); console.log('Generated proof-of-possession signature'); // Build contract call to register-user-secure const contractCall = await makeContractCall({ contractAddress: CONFIG.STACKS_CONTRACT_ADDRESS, contractName: CONFIG.STACKS_CONTRACT_NAME, functionName: 'register-user-secure', functionArgs: [ standardPrincipalCV(userAddress), // user principal uintCV(Buffer.from(publicKey, 'hex')), // public key (33 bytes) uintCV(Buffer.from(signature, 'hex')), // challenge signature (65 bytes) ], network: this.network, senderKey: privateKeyId, // This will be resolved by TurnkeyHQ }); console.log('Built contract call for user registration'); // Sign and broadcast the transaction const signedTx = await this.signTransaction(contractCall, subOrganizationId, privateKeyId); const result = await broadcastTransaction(signedTx, this.network); console.log(`User registration successful: ${result.txid}`); return result; } catch (error) { console.error('Failed to register user on contract:', error); throw error; } } /** * Step 3: Execute a purchase transaction * * This demonstrates how to execute a purchase using the embedded wallet * contract's execute-verified-transaction function with proper policy * enforcement and post-conditions. */ async executePurchase( userAddress: string, recipient: string, amount: number, // in microSTX productId: string, subOrganizationId: string, privateKeyId: string, userNonce: number ) { console.log(`Executing purchase: ${amount} microSTX to ${recipient}`); try { // Get current user data from contract to verify nonce const userData = await this.getUserData(userAddress); if (!userData) { throw new Error('User not registered'); } // Create transaction message to sign const transactionMessage = this.createTransactionMessage({ userAddress, recipient, amount, productId, nonce: userNonce, }); // Sign the transaction message with TurnkeyHQ const signature = await this.sdk.signRawPayload({ organizationId: subOrganizationId, privateKeyId: privateKeyId, payload: transactionMessage, encoding: 'PAYLOAD_ENCODING_HEXADECIMAL', }); console.log('Signed transaction message'); // Build contract call to execute-verified-transaction const contractCall = await makeContractCall({ contractAddress: CONFIG.STACKS_CONTRACT_ADDRESS, contractName: CONFIG.STACKS_CONTRACT_NAME, functionName: 'execute-verified-transaction', functionArgs: [ uintCV(amount), // amount in microSTX standardPrincipalCV(recipient), // recipient uintCV(Buffer.from(transactionMessage, 'hex')), // message hash uintCV(Buffer.from(signature, 'hex')), // signature uintCV(userNonce), // expected nonce ], network: this.network, senderKey: privateKeyId, postConditions: [ // MANDATORY: Post-condition for SIP-010 compliance makeStandardFungiblePostCondition( userAddress, FungibleConditionCode.Equal, amount, createAssetInfo( CONFIG.STACKS_CONTRACT_ADDRESS, CONFIG.STACKS_CONTRACT_NAME, 'embedded-wallet-token' ) ), ], postConditionMode: PostConditionMode.Deny, }); console.log('Built contract call for purchase execution'); // Sign and broadcast the transaction const signedTx = await this.signTransaction(contractCall, subOrganizationId, privateKeyId); const result = await broadcastTransaction(signedTx, this.network); console.log(`Purchase executed successfully: ${result.txid}`); return { txId: result.txid, explorerUrl: `https://explorer.stacks.co/txid/${result.txid}`, amount, recipient, productId, }; } catch (error) { console.error('Failed to execute purchase:', error); throw error; } } /** * Step 4: Transfer STX using SIP-010 compliant transfer function * * This demonstrates how to use the native fungible token transfer * function with mandatory post-conditions. */ async transferSTX( senderAddress: string, recipient: string, amount: number, // in microSTX subOrganizationId: string, privateKeyId: string, memo?: string ) { console.log(`Transferring ${amount} microSTX from ${senderAddress} to ${recipient}`); try { // Build SIP-010 compliant transfer transaction const transferTx = await makeContractCall({ contractAddress: CONFIG.STACKS_CONTRACT_ADDRESS, contractName: CONFIG.STACKS_CONTRACT_NAME, functionName: 'transfer', functionArgs: [ uintCV(amount), // amount standardPrincipalCV(senderAddress), // sender standardPrincipalCV(recipient), // recipient memo ? uintCV(Buffer.from(memo, 'utf8')) : noneCV(), // memo (optional) ], network: this.network, senderKey: privateKeyId, postConditions: [ // MANDATORY: Post-condition for SIP-010 compliance makeStandardFungiblePostCondition( senderAddress, FungibleConditionCode.Equal, amount, createAssetInfo( CONFIG.STACKS_CONTRACT_ADDRESS, CONFIG.STACKS_CONTRACT_NAME, 'embedded-wallet-token' ) ), ], postConditionMode: PostConditionMode.Deny, }); console.log('Built SIP-010 compliant transfer transaction'); // Sign and broadcast the transaction const signedTx = await this.signTransaction(transferTx, subOrganizationId, privateKeyId); const result = await broadcastTransaction(signedTx, this.network); console.log(`STX transfer successful: ${result.txid}`); return { txId: result.txid, explorerUrl: `https://explorer.stacks.co/txid/${result.txid}`, amount, recipient, }; } catch (error) { console.error('Failed to transfer STX:', error); throw error; } } /** * Helper method to sign transactions with TurnkeyHQ */ private async signTransaction( transaction: any, subOrganizationId: string, privateKeyId: string ) { try { // Serialize the transaction const serializedTx = transaction.serialize(); // Sign with TurnkeyHQ const signature = await this.sdk.signRawPayload({ organizationId: subOrganizationId, privateKeyId: privateKeyId, payload: serializedTx.toString('hex'), encoding: 'PAYLOAD_ENCODING_HEXADECIMAL', }); // Apply signature to transaction transaction.setSignature(signature); return transaction; } catch (error) { console.error('Failed to sign transaction:', error); throw error; } } /** * Helper method to create transaction message for signing */ private createTransactionMessage(params: { userAddress: string; recipient: string; amount: number; productId: string; nonce: number; }): string { const message = JSON.stringify({ userAddress: params.userAddress, recipient: params.recipient, amount: params.amount, productId: params.productId, nonce: params.nonce, timestamp: Date.now(), }); return Buffer.from(message, 'utf8').toString('hex'); } /** * Helper method to get user data from contract */ private async getUserData(userAddress: string): Promise<any> { // This would typically call a read-only function on your contract // Implementation depends on your specific contract structure console.log(`Getting user data for: ${userAddress}`); // Return mock data for example return { nonce: 0, maxAmount: 1000000000, // 1 STX in microSTX dailyLimit: 10000000000, // 10 STX in microSTX isRegistered: true, }; } } /** * Example usage of the TurnkeyStacksIntegration class */ export async function exampleUsage() { const integration = new TurnkeyStacksIntegration(); try { // Step 1: Create user wallet const wallet = await integration.createUserWallet( 'user-123', 'user@example.com' ); console.log('Wallet created:', wallet); // Step 2: Register user on contract await integration.registerUserOnContract( wallet.address, wallet.publicKey, wallet.subOrganizationId, wallet.privateKeyId ); // Step 3: Execute a purchase const purchaseResult = await integration.executePurchase( wallet.address, 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9', // recipient 500000, // 0.5 STX in microSTX 'premium-nft-001', wallet.subOrganizationId, wallet.privateKeyId, 0 // user nonce ); console.log('Purchase completed:', purchaseResult); // Step 4: Transfer STX using SIP-010 function const transferResult = await integration.transferSTX( wallet.address, 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9', 250000, // 0.25 STX in microSTX wallet.subOrganizationId, wallet.privateKeyId, 'Payment for services' ); console.log('Transfer completed:', transferResult); } catch (error) { console.error('Example usage failed:', error); } } // Export the integration class for use in other modules export default TurnkeyStacksIntegration;

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/exponentlabshq/stacks-clarity-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server