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
309 changes: 309 additions & 0 deletions ANCHOR_REFACTORING_GUIDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
# Refactoring Guide: Solana Contract Integration with Anchor

This guide demonstrates how to refactor your existing Solana contract integration to use the Anchor framework, making your code more type-safe and maintainable.

## 1. Adding Required Dependencies

First, ensure you have all the necessary dependencies in your project:

```bash
npm install @coral-xyz/anchor @project-serum/anchor
```

## 2. Utility Files Created/Modified

The following files were created or modified to support the Anchor integration:

- `apps/server/src/utils/transaction-anchor.utils.ts` - Core Anchor transaction utilities
- `apps/server/src/utils/parse-anchor.utils.ts` - Parse and convert data for Anchor
- `apps/server/src/solana-contract/anchor/anchor.service.ts` - Service for Anchor program interactions
- `apps/server/src/solana-contract/escrow/escrow-anchor.service.ts` - Anchor-powered escrow service

## 3. Implementation Examples

### Example 1: Fund Escrow

**Before** (using raw Web3.js):

```typescript
async fundEscrow(contractId: string, signer: string, amount: string): Promise<ApiResponse> {
const escrowAccount = new PublicKey(contractId);
const signPublicKey = new PublicKey(signer);
const programId = new PublicKey(apiConfig.solanaProgramId);

const payloadBuffer = Buffer.alloc(9);
payloadBuffer.writeUInt8(0, 0); // instruction type

// Convert amount to BigInt and write as 64-bit integer
const amountBigInt = BigInt(amount);
for (let i = 0; i < 8; i++) {
payloadBuffer.writeUInt8(
Number((amountBigInt >> BigInt(i * 8)) & BigInt(0xff)),
i + 1,
);
}

const instruction = new TransactionInstruction({
keys: [
{ pubkey: escrowAccount, isSigner: false, isWritable: true },
{ pubkey: signPublicKey, isSigner: true, isWritable: true },
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
],
programId,
data: payloadBuffer,
});

const transaction = new Transaction().add(instruction);
// ... rest of the code
}
```

**After** (using Anchor):

```typescript
async fundEscrow(contractId: string, signer: string, amount: string): Promise<ApiResponse> {
// Convert addresses to PublicKeys
const escrowAccount = new PublicKey(contractId);
const signerPublicKey = new PublicKey(signer);

// Get the Anchor program instance
const program = this.anchorService.getEscrowProgram();

// Convert amount string to BN for Anchor
const amountBN = toBN(amount);

// Build transaction using Anchor
const { serializedTransaction, txId } = await buildAnchorTransaction({
program,
methodName: 'fundEscrow',
args: [amountBN],
accounts: {
escrowAccount,
signer: signerPublicKey,
systemProgram: SystemProgram.programId,
},
feePayer: signerPublicKey,
});

// Add to the pending write queue
this.pendingWriteQueue.add(txId, {
type: 'FUND_ESCROW',
payload: { contractId, amount },
});

// Return the transaction for signing
return {
status: 'success',
unsignedTransaction: serializedTransaction,
};
}
```

### Example 2: Change Dispute Flag

**Before** (using raw Web3.js or Stellar SDK):

```typescript
async changeDisputeFlag(contractId: string, signer: string): Promise<ApiResponse> {
const contract = new StellarSDK.Contract(contractId);
const account = await this.sorobanServer.getAccount(signer);

const operations = [contract.call('change_dispute_flag')];

const transaction = buildTransaction(account, operations);
const preparedTransaction = await this.sorobanServer.prepareTransaction(transaction);

// ... rest of the code
}
```

**After** (using Anchor):

```typescript
async changeDisputeFlag(contractId: string, signer: string): Promise<ApiResponse> {
// Convert addresses to PublicKeys
const escrowAccount = new PublicKey(contractId);
const signerPublicKey = new PublicKey(signer);

// Get the Anchor program instance
const program = this.anchorService.getEscrowProgram();

// Build transaction using Anchor
const { serializedTransaction, txId } = await buildAnchorTransaction({
program,
methodName: 'changeDisputeFlag',
args: [],
accounts: {
signer: signerPublicKey,
escrowAccount,
},
feePayer: signerPublicKey,
});

// Add to the pending write queue
this.pendingWriteQueue.add(txId, {
type: 'START_DISPUTE',
payload: { contractId },
});

// Return the transaction for signing
return {
status: 'success',
unsignedTransaction: serializedTransaction,
};
}
```

### Example 3: Fetching Account Data

**Before** (using raw account deserialization):

```typescript
async getEscrowByContractID(signer, contractId: string): Promise<EscrowCamelCaseResponse> {
const contract = new StellarSDK.Contract(this.trustlessContractId);
const account = await this.horizonServer.loadAccount(signer);

const operations = [
contract.call(
'get_escrow_by_contract_id',
StellarSDK.Address.fromString(contractId).toScVal(),
),
];

const transaction = buildTransaction(account, operations);
const preparedTransaction = await this.sorobanServer.prepareTransaction(transaction);
const result = await this.sorobanServer.simulateTransaction(preparedTransaction);

const retval = result.result?.retval;
if (!retval) {
throw new Error('Could not get return value from contract.');
}

return parseEscrowByContractId(retval);
}
```

**After** (using Anchor):

```typescript
async getEscrowByContractID(signer: string, contractId: string): Promise<EscrowCamelCaseResponse> {
// Convert address to PublicKey
const escrowAccountPubkey = new PublicKey(contractId);

// Get the Anchor program
const program = this.anchorService.getEscrowProgram();

// Fetch the account data using Anchor's typed fetching
const escrowAccount = await program.account.escrowAccount.fetch(escrowAccountPubkey);

// Convert Anchor account data to your API response format
return {
approver: escrowAccount.approver.toString(),
receiver: escrowAccount.receiver.toString(),
amount: fromBN(escrowAccount.amount),
title: escrowAccount.title || '',
description: escrowAccount.description || '',
status: escrowAccount.status || 'PENDING',
disputeFlag: escrowAccount.disputeFlag || false,
trustlineDecimals: 6, // Default for USDC
// Map other fields as needed
};
}
```

## 4. Key Utility Functions

### Building Transactions

```typescript
// From transaction-anchor.utils.ts
export async function buildAnchorTransaction({
program,
methodName,
args = [],
accounts,
feePayer,
}: {
program: Program;
methodName: string;
args?: any[];
accounts: Record<string, PublicKey>;
feePayer: PublicKey;
}): Promise<{
transaction: Transaction;
serializedTransaction: string;
txId: string;
}> {
// Build transaction using Anchor's fluent interface
const tx = await program.methods[methodName](...args)
.accounts(accounts)
.transaction();

// Set fee payer
tx.feePayer = feePayer;

// Get recent blockhash
const { blockhash } =
await program.provider.connection.getLatestBlockhash("confirmed");
tx.recentBlockhash = blockhash;

// Serialize for client-side signing
const serializedTx = tx.serialize({
requireAllSignatures: false,
verifySignatures: false,
});

// Create transaction ID hash for tracking
const txId = createHash("sha256").update(serializedTx).digest("hex");

return {
transaction: tx,
serializedTransaction: serializedTx.toString("base64"),
txId,
};
}
```

### Converting Data Types

```typescript
// From parse-anchor.utils.ts
export function toBN(amount: string | number): anchor.BN {
try {
return new anchor.BN(amount);
} catch (error) {
console.error("Error converting to BN:", error);
return new anchor.BN(0);
}
}

export function fromBN(bn: anchor.BN | null | undefined): string {
if (!bn) return "0";
return bn.toString();
}

export function toMicroUSDC(
amount: string | number,
decimals: number = 6,
): anchor.BN {
const amountStr = amount.toString();
const [integerPart, fractionalPart = ""] = amountStr.split(".");

let paddedFractional = fractionalPart
.padEnd(decimals, "0")
.slice(0, decimals);
const microAmount = integerPart + paddedFractional;

return new anchor.BN(microAmount);
}
```

## 5. Next Steps

1. Use the examples above to refactor all remaining methods in your escrow service
2. Update the controller to inject the EscrowAnchorService where needed
3. Consider a phased approach where you migrate one method at a time
4. Add proper error handling for Anchor program errors
5. Write tests to verify the refactored methods work as expected

This refactoring makes your code more robust through compile-time type checking, automatic account deserialization, and simpler transaction building.
2 changes: 1 addition & 1 deletion apps/server/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@ import { SolanaContractModule } from './solana-contract/solana-contract.module'
},
],
})
export class AppModule { }
export class AppModule {}
70 changes: 70 additions & 0 deletions apps/server/src/solana-contract/anchor/anchor.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import * as anchor from '@coral-xyz/anchor'
import { Program } from '@coral-xyz/anchor'
import { Injectable } from '@nestjs/common'
import { Connection, Keypair, PublicKey } from '@solana/web3.js'
import { apiConfig } from 'src/config/api.config'

@Injectable()
export class AnchorService {
private readonly connection: Connection
private readonly escrowProgramId: PublicKey

constructor() {
// Initialize connection
this.connection = new Connection(apiConfig.solanaServerURL, 'confirmed')

// Set program IDs from config
this.escrowProgramId = new PublicKey(apiConfig.solanaProgramId)

// Load IDL
try {
// The IDL is available in the @programs/solana-tl package
const { EscrowIDL } = require('@programs/solana-tl')
this._escrowIdl = EscrowIDL
Copy link

Copilot AI May 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider providing an explicit type for _escrowIdl instead of using 'any' to improve type-safety and maintainability.

Copilot uses AI. Check for mistakes.
} catch (error) {
console.error('Failed to load IDL from package:', error)
}
}
Comment on lines +20 to +27
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Guard against missing or invalid IDL

require('@programs/solana-tl') is wrapped in a try / catch, but if the IDL fails to load _escrowIdl remains undefined.
Every later call to new anchor.Program(this._escrowIdl, …) will then crash at runtime.

Add an explicit guard (or re-throw) so the service fails fast instead of propagating a cryptic error much later.

-			const { EscrowIDL } = require('@programs/solana-tl')
-			this._escrowIdl = EscrowIDL
+			const { EscrowIDL } = require('@programs/solana-tl')
+			if (!EscrowIDL) throw new Error('EscrowIDL not found in package')
+			this._escrowIdl = EscrowIDL
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
// The IDL is available in the @programs/solana-tl package
const { EscrowIDL } = require('@programs/solana-tl')
this._escrowIdl = EscrowIDL
} catch (error) {
console.error('Failed to load IDL from package:', error)
}
}
try {
// The IDL is available in the @programs/solana-tl package
const { EscrowIDL } = require('@programs/solana-tl')
if (!EscrowIDL) throw new Error('EscrowIDL not found in package')
this._escrowIdl = EscrowIDL
} catch (error) {
console.error('Failed to load IDL from package:', error)
}
🤖 Prompt for AI Agents
In apps/server/src/solana-contract/anchor/anchor.service.ts around lines 20 to
27, the code catches errors when requiring the IDL but does not handle the case
where _escrowIdl remains undefined, leading to runtime crashes later. To fix
this, after the try/catch block, add an explicit check to verify that _escrowIdl
is defined; if not, throw a clear error or re-throw the caught error to fail
fast and prevent later cryptic failures when using the IDL.


/**
* Get an AnchorProvider with optional wallet
*/
getProvider(wallet?: anchor.Wallet): anchor.AnchorProvider {
return new anchor.AnchorProvider(this.connection, wallet || null, {
commitment: 'confirmed',
})
}
Comment on lines +32 to +36
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

wallet may not be optional for mutating calls

new anchor.AnchorProvider(this.connection, wallet || null, …) creates a provider with a null wallet.
As soon as you invoke any RPC that needs a signer (all mutation flows in EscrowAnchorService), Anchor will throw “provider.wallet is null”.

Two options:

  1. Require the caller to supply a wallet when a signed tx is needed.
  2. Lazily create a read-only “dummy” provider only for .fetch() operations and expose a separate method for signed providers.

Clarifying this now prevents difficult‐to-trace runtime failures.

🤖 Prompt for AI Agents
In apps/server/src/solana-contract/anchor/anchor.service.ts around lines 32 to
36, the getProvider method currently allows creating an AnchorProvider with a
null wallet, which causes runtime errors when signing is required. To fix this,
either make the wallet parameter mandatory for all calls that require signing or
separate the logic by creating one method that returns a read-only provider with
no wallet for fetch-only operations and another method that requires a wallet
for signed transactions. This ensures that mutation calls always have a valid
wallet and prevents null wallet errors.


/**
* Create a wallet from a private key
*/
createWallet(privateKey: Uint8Array): anchor.Wallet {
const keypair = Keypair.fromSecretKey(privateKey)
return new anchor.Wallet(keypair)
}

/**
* Get the escrow program with optional wallet
*/
getEscrowProgram(wallet?: anchor.Wallet): Program {
const provider = this.getProvider(wallet)
return new anchor.Program(this._escrowIdl, this.escrowProgramId, provider)
}
Comment on lines +49 to +52
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Type safety & early failure for _escrowIdl

_escrowIdl is typed as any; the compiler cannot help when the wrong IDL shape is supplied.
Declare a concrete type (anchor.Idl) and assert non-null before instantiating the program:

-	private _escrowIdl: any
+	private _escrowIdl!: anchor.Idl

and

if (!this._escrowIdl)
  throw new Error('Escrow IDL is undefined');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
getEscrowProgram(wallet?: anchor.Wallet): Program {
const provider = this.getProvider(wallet)
return new anchor.Program(this._escrowIdl, this.escrowProgramId, provider)
}
// In apps/server/src/solana-contract/anchor/anchor.service.ts
export class AnchorService {
// …
- private _escrowIdl: any;
+ private _escrowIdl!: anchor.Idl;
getEscrowProgram(wallet?: anchor.Wallet): Program {
const provider = this.getProvider(wallet);
+ if (!this._escrowIdl) {
+ throw new Error('Escrow IDL is undefined');
+ }
return new anchor.Program(
this._escrowIdl,
this.escrowProgramId,
provider,
);
}
// …
}
🤖 Prompt for AI Agents
In apps/server/src/solana-contract/anchor/anchor.service.ts around lines 49 to
52, the _escrowIdl property is typed as any, which prevents type safety and
early error detection. Change the type of _escrowIdl to anchor.Idl to enforce
the correct IDL shape, and add a non-null assertion or explicit check before
using it to instantiate the Program, ensuring the value is valid and preventing
runtime errors.


/**
* Get the connection
*/
getConnection(): Connection {
return this.connection
}

/**
* Get the escrow program ID
*/
getEscrowProgramId(): PublicKey {
return this.escrowProgramId
}

// Private fields
private _escrowIdl: any
}
Loading