Skip to content

Issue #4: Create Transaction Builder Wrapper for Account Abstraction #19

@wheval

Description

@wheval

Create Transaction Builder Wrapper for Account Abstraction

Description:
Build a high-level transaction builder that wraps Stellar SDK's TransactionBuilder and simplifies account abstraction operations. This is NOT a replacement for Stellar's builder - it's a convenience wrapper focused on smart account operations.

Context:
The Stellar SDK already has an excellent TransactionBuilder with fluent API. However, invoking our account abstraction smart contract methods (add_session_key, execute, revoke_session_key) requires verbose Soroban contract invocation code. This wrapper provides a clean API specifically for our account abstraction layer while delegating to Stellar SDK's builder under the hood.

Important: This uses Stellar SDK's TransactionBuilder internally. We're NOT reimplementing transaction building - we're creating convenience methods for our specific contract operations.

Requirements:

  • Create AccountTransactionBuilder wrapper class using Stellar SDK's TransactionBuilder internally
  • Implement .addSessionKey() method (wraps contract invocation for add_session_key)
  • Implement .revokeSessionKey() method (wraps contract invocation for revoke_session_key)
  • Implement .execute() method (wraps contract invocation for execute with session key)
  • Add .simulate() method (required for Soroban transactions before submission)
  • Support standard Stellar operations via .addOperation() passthrough
  • Automatic fee estimation from simulation results
  • Add memo support (delegate to underlying TransactionBuilder)
  • Implement error handling with actionable messages
  • Unit tests for all wrapper methods (>90% coverage)
  • Integration tests building and submitting to testnet

Implementation Guide:

import * as StellarSdk from '@stellar/stellar-sdk';
import { Contract } from '@stellar/stellar-sdk';

export class AccountTransactionBuilder {
  private txBuilder: StellarSdk.TransactionBuilder;
  private server: StellarSdk.SorobanRpc.Server;
  private contractId: string;

  constructor(
    sourceAccount: StellarSdk.Account,
    server: StellarSdk.SorobanRpc.Server,
    accountContractId: string,
    networkPassphrase: string
  ) {
    // Use Stellar SDK's TransactionBuilder internally
    this.txBuilder = new StellarSdk.TransactionBuilder(sourceAccount, {
      fee: StellarSdk.BASE_FEE,
      networkPassphrase
    });
    this.server = server;
    this.contractId = accountContractId;
  }

  // Convenience method for adding session key
  addSessionKey(
    publicKey: string,
    permissions: number[],
    expiresAt: number
  ): this {
    const contract = new Contract(this.contractId);
    
    // Build Soroban contract invocation
    const operation = contract.call(
      'add_session_key',
      StellarSdk.xdr.ScVal.scvAddress(
        StellarSdk.Address.fromString(publicKey).toScAddress()
      ),
      StellarSdk.xdr.ScVal.scvVec(
        permissions.map(p => StellarSdk.xdr.ScVal.scvU32(p))
      ),
      StellarSdk.xdr.ScVal.scvU64(new StellarSdk.xdr.Uint64(expiresAt))
    );
    
    this.txBuilder.addOperation(operation);
    return this;
  }

  // Convenience method for revoking session key
  revokeSessionKey(publicKey: string): this {
    const contract = new Contract(this.contractId);
    
    const operation = contract.call(
      'revoke_session_key',
      StellarSdk.xdr.ScVal.scvAddress(
        StellarSdk.Address.fromString(publicKey).toScAddress()
      )
    );
    
    this.txBuilder.addOperation(operation);
    return this;
  }

  // Convenience method for executing with session key
  execute(
    sessionKeyPublicKey: string,
    operations: StellarSdk.xdr.Operation[]
  ): this {
    const contract = new Contract(this.contractId);
    
    const operation = contract.call(
      'execute',
      StellarSdk.xdr.ScVal.scvAddress(
        StellarSdk.Address.fromString(sessionKeyPublicKey).toScAddress()
      ),
      StellarSdk.xdr.ScVal.scvVec(
        operations.map(op => /* encode operation */)
      )
    );
    
    this.txBuilder.addOperation(operation);
    return this;
  }

  // Add memo (delegate to underlying builder)
  addMemo(memo: StellarSdk.Memo): this {
    this.txBuilder.addMemo(memo);
    return this;
  }

  // Passthrough for any standard Stellar operation
  addOperation(operation: StellarSdk.xdr.Operation): this {
    this.txBuilder.addOperation(operation);
    return this;
  }

  // Simulate transaction (REQUIRED for Soroban)
  async simulate(): Promise<StellarSdk.SorobanRpc.Api.SimulateTransactionResponse> {
    const tx = this.txBuilder.build();
    return await this.server.simulateTransaction(tx);
  }

  // Build final transaction with simulation data
  async build(): Promise<StellarSdk.Transaction> {
    // Simulate first to get resource footprint
    const simulation = await this.simulate();
    
    if (StellarSdk.SorobanRpc.Api.isSimulationSuccess(simulation)) {
      // Apply simulation results to transaction
      const tx = this.txBuilder.build();
      return StellarSdk.SorobanRpc.assembleTransaction(tx, simulation).build();
    } else {
      throw new Error(`Simulation failed: ${simulation.error}`);
    }
  }
}

Usage:

import { AccountTransactionBuilder } from '@ancore/core-sdk';
import { StellarClient } from '@ancore/stellar';

// Initialize
const client = new StellarClient('testnet');
const sourceAccount = await client.getAccount(publicKey);
const server = client.server; // Expose SorobanRpc.Server

// Add session key
const builder = new AccountTransactionBuilder(
  sourceAccount,
  server,
  'CABC...', // account contract ID
  StellarSdk.Networks.TESTNET
);

const tx = await builder
  .addSessionKey(
    sessionKeyPair.publicKey(),
    [0, 1], // permissions: SEND_PAYMENT, MANAGE_DATA
    Date.now() + 3600000 // expires in 1 hour
  )
  .addMemo(StellarSdk.Memo.text('Add session key'))
  .build(); // Automatically simulates

// Sign and submit
tx.sign(masterKeypair);
const result = await client.submitTransaction(tx);

// Revoke session key
const revokeTx = await new AccountTransactionBuilder(...)
  .revokeSessionKey(sessionKeyPair.publicKey())
  .build();

Key Principles:

  1. Wrapper, not replacement - Uses StellarSdk.TransactionBuilder internally
  2. Convenience methods - Simplifies contract invocations
  3. Automatic simulation - Soroban requires simulation before submission
  4. Fluent API - Chain methods just like Stellar SDK
  5. Passthrough support - Can still add any standard Stellar operation

Files to Create:

  • packages/core-sdk/src/account-transaction-builder.ts (main wrapper class)
  • packages/core-sdk/src/contract-params.ts (helpers for encoding contract params)
  • packages/core-sdk/src/errors.ts (custom error types for simulation failures)
  • packages/core-sdk/src/__tests__/builder.test.ts (unit tests)
  • packages/core-sdk/src/__tests__/integration.test.ts (testnet tests)
  • packages/core-sdk/README.md (explain wrapper vs Stellar SDK builder)

Dependencies:

  • @stellar/stellar-sdk (^12.0.0) - The underlying TransactionBuilder
  • @ancore/types (workspace) - SmartAccount, SessionKey types
  • @ancore/stellar (workspace) - StellarClient

Success Criteria:

  • Wrapper successfully uses Stellar SDK's TransactionBuilder internally
  • All contract invocations (add/revoke session key, execute) work
  • Simulation runs automatically before build()
  • Fee estimation from simulation applied correctly
  • Error messages explain simulation failures clearly
  • Can still add standard Stellar operations via passthrough

Definition of Done:

  • All account contract methods wrapped (add_session_key, revoke_session_key, execute)
  • Automatic simulation before build()
  • Tests cover successful and failed simulations
  • Documentation clearly explains this is a wrapper, not a replacement
  • Integration tests pass with testnet contract deployment
  • Examples show both convenience methods and passthrough operations

Key Principle:

This is a WRAPPER, not a replacement! We use StellarSdk.TransactionBuilder internally. Only add convenience methods for our account abstraction contract operations.

Additional Resources:

Labels: sdk, transaction, developer-experience
Complexity: 200 points (High)
Estimated Effort: 4-5 days
Priority: High


Questions or need help? Join our Telegram community

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions