adding 2/2 multisig \w agent & user (#88)

# Pull Request Description

## Related Issue
Fixes #149 

## Changes Made
This PR adds the following changes:
- added create squads multisig
- deposit to multisig
- tx from multisig
- create proposal from multisig
- approve proposal from multisig
- reject proposal from multisig
- exexute proposal from multisig
  
## Implementation Details
<!-- Provide technical details about the implementation -->
- added `@sqds/multisig` sdk

## Tests are added in comment below

## Transaction Links:

https://explorer.solana.com/tx/4weMdQnnkV3SW3JAngdpZz3WEeugWni2ep2MP71KTbtLGHTLF7ok2EVKZP9Ug8T4twqr9HsB3tjLUyrGLnngzYVS?cluster=devnet

https://explorer.solana.com/tx/4QUxE5VQGWZw1LCYbj3BYtzGfn2o45Zk71K8LBkTva8hAdv4o1jUNEwWmuQRMJpXY9yKKwiwD59tZRh6oXgd4Jzc?cluster=devnet

https://explorer.solana.com/tx/TVuzkLBkCtJ5PvroXLnnW1PTeLMQ4R9NeoiHeoHhsGR2gJX3cLjq1Ndq6DVD72dQLLbJM6CqnJZDYu9VSp4YbxV?cluster=devnet

https://explorer.solana.com/tx/62PDfY46rGFezTnVCbDHUeCMyoVCEfWdGJWkkDhFL1GoAcfznWgCYXo2DnSgpgZu59vvYuaNj5C32bjAkNnJZvtJ?cluster=devnet

https://explorer.solana.com/tx/3Pxvyo2MVEEbqcPcQ6Q6ErFTEu9EGHPwgciMuXUXzLsB8XTdyCZbxfrscxJbCbW8DgRVA6AomTXX6Uup9Bnubzpu?cluster=devnet

https://explorer.solana.com/tx/5MyWxoTUzL3Zb4rStXzqsX1Cp3L3ToQdYyPq2sDDHBHsJV9idoHZYcKQjzfmgkGdwg3mtJZdUeywuTGvhcdvzkGU?cluster=devnet

https://explorer.solana.com/tx/3haMJ9EoLfDA7H5ACPb1K1A9Eyezfx9DtB2eCUgNE38CkQd95KtZNEjRopaMva2bqtRvTzRNqkLVvbVcu2Gjzphs?cluster=devnet

https://explorer.solana.com/tx/4Rn1AD9bUGwQzN661pexyPEffp25bUujRUXk18NvfxinZD12rrBLdeASdtnb7QnDfgvwkfzwk2akgU4Sf8ek7XwR?cluster=devnet

## Checklist
- [x] I have tested these changes locally
- [x] I have updated the documentation
- [x] I have added a transaction link
- [x] I have added the prompt used to test it
This commit is contained in:
aryan
2025-01-10 05:18:08 +05:30
committed by GitHub
13 changed files with 1137 additions and 354 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -48,6 +48,7 @@
"@solana/spl-token": "^0.4.9",
"@solana/web3.js": "^1.98.0",
"@tensor-oss/tensorswap-sdk": "^4.5.0",
"@sqds/multisig": "^2.1.3",
"@tiplink/api": "^0.3.1",
"ai": "^4.0.22",
"bn.js": "^5.2.1",

695
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -85,6 +85,13 @@ import {
CreateSingleOptions,
StoreInitOptions,
} from "@3land/listings-sdk/dist/types/implementation/implementationTypes";
import { create_squads_multisig } from "../tools/squads_multisig/create_multisig";
import { deposit_to_multisig } from "../tools/squads_multisig/deposit_to_multisig";
import { transfer_from_multisig } from "../tools/squads_multisig/transfer_from_multisig";
import { create_proposal } from "../tools/squads_multisig/create_proposal";
import { approve_proposal } from "../tools/squads_multisig/approve_proposal";
import { execute_transaction } from "../tools/squads_multisig/execute_proposal";
import { reject_proposal } from "../tools/squads_multisig/reject_proposal";
/**
* Main class for interacting with Solana blockchain
@@ -603,4 +610,49 @@ export class SolanaAgentKit {
);
return `Transaction: ${tx}`;
}
async createSquadsMultisig(creator: PublicKey): Promise<string> {
return create_squads_multisig(this, creator);
}
async depositToMultisig(
amount: number,
vaultIndex: number = 0,
mint?: PublicKey,
): Promise<string> {
return deposit_to_multisig(this, amount, vaultIndex, mint);
}
async transferFromMultisig(
amount: number,
to: PublicKey,
vaultIndex: number = 0,
mint?: PublicKey,
): Promise<string> {
return transfer_from_multisig(this, amount, to, vaultIndex, mint);
}
async createMultisigProposal(
transactionIndex?: number | bigint,
): Promise<string> {
return create_proposal(this, transactionIndex);
}
async approveMultisigProposal(
transactionIndex?: number | bigint,
): Promise<string> {
return approve_proposal(this, transactionIndex);
}
async rejectMultisigProposal(
transactionIndex?: number | bigint,
): Promise<string> {
return reject_proposal(this, transactionIndex);
}
async executeMultisigTransaction(
transactionIndex?: number | bigint,
): Promise<string> {
return execute_transaction(this, transactionIndex);
}
}

View File

@@ -2435,6 +2435,259 @@ export class SolanaCloseEmptyTokenAccounts extends Tool {
}
}
export class SolanaCreate2by2Multisig extends Tool {
name = "create_2by2_multisig";
description = `Create a 2-of-2 multisig account on Solana with the user and the agent, where both approvals will be required to run the transactions.
Note: For one AI agent, only one 2-by-2 multisig can be created as it is pair-wise.
Inputs (JSON string):
- creator: string, the public key of the creator (required).`;
constructor(private solanaKit: SolanaAgentKit) {
super();
}
protected async _call(input: string): Promise<string> {
try {
const inputFormat = JSON.parse(input);
const creator = new PublicKey(inputFormat.creator);
const multisig = await this.solanaKit.createSquadsMultisig(creator);
return JSON.stringify({
status: "success",
message: "2-by-2 multisig account created successfully",
multisig,
});
} catch (error: any) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "CREATE_2BY2_MULTISIG_ERROR",
});
}
}
}
export class SolanaDepositTo2by2Multisig extends Tool {
name = "deposit_to_2by2_multisig";
description = `Deposit funds to a 2-of-2 multisig account on Solana with the user and the agent, where both approvals will be required to run the transactions.
Inputs (JSON string):
- amount: number, the amount to deposit in SOL (required).`;
constructor(private solanaKit: SolanaAgentKit) {
super();
}
protected async _call(input: string): Promise<string> {
try {
const inputFormat = JSON.parse(input);
const amount = new Decimal(inputFormat.amount);
const tx = await this.solanaKit.depositToMultisig(amount.toNumber());
return JSON.stringify({
status: "success",
message: "Funds deposited to 2-by-2 multisig account successfully",
transaction: tx,
amount: amount.toString(),
});
} catch (error: any) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "DEPOSIT_TO_2BY2_MULTISIG_ERROR",
});
}
}
}
export class SolanaTransferFrom2by2Multisig extends Tool {
name = "transfer_from_2by2_multisig";
description = `Create a transaction to transfer funds from a 2-of-2 multisig account on Solana with the user and the agent, where both approvals will be required to run the transactions.
Inputs (JSON string):
- amount: number, the amount to transfer in SOL (required).
- recipient: string, the public key of the recipient (required).`;
constructor(private solanaKit: SolanaAgentKit) {
super();
}
protected async _call(input: string): Promise<string> {
try {
const inputFormat = JSON.parse(input);
const amount = new Decimal(inputFormat.amount);
const recipient = new PublicKey(inputFormat.recipient);
const tx = await this.solanaKit.transferFromMultisig(
amount.toNumber(),
recipient,
);
return JSON.stringify({
status: "success",
message: "Transaction added to 2-by-2 multisig account successfully",
transaction: tx,
amount: amount.toString(),
recipient: recipient.toString(),
});
} catch (error: any) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "TRANSFER_FROM_2BY2_MULTISIG_ERROR",
});
}
}
}
export class SolanaCreateProposal2by2Multisig extends Tool {
name = "create_proposal_2by2_multisig";
description = `Create a proposal to transfer funds from a 2-of-2 multisig account on Solana with the user and the agent, where both approvals will be required to run the transactions.
If transactionIndex is not provided, the latest index will automatically be fetched and used.
Inputs (JSON string):
- transactionIndex: number, the index of the transaction (optional).`;
constructor(private solanaKit: SolanaAgentKit) {
super();
}
protected async _call(input: string): Promise<string> {
try {
const inputFormat = JSON.parse(input);
const transactionIndex = inputFormat.transactionIndex ?? undefined;
const tx = await this.solanaKit.createMultisigProposal(transactionIndex);
return JSON.stringify({
status: "success",
message: "Proposal created successfully",
transaction: tx,
transactionIndex: transactionIndex?.toString(),
});
} catch (error: any) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "CREATE_PROPOSAL_2BY2_MULTISIG_ERROR",
});
}
}
}
export class SolanaApproveProposal2by2Multisig extends Tool {
name = "approve_proposal_2by2_multisig";
description = `Approve a proposal to transfer funds from a 2-of-2 multisig account on Solana with the user and the agent, where both approvals will be required to run the transactions.
If proposalIndex is not provided, the latest index will automatically be fetched and used.
Inputs (JSON string):
- proposalIndex: number, the index of the proposal (optional).`;
constructor(private solanaKit: SolanaAgentKit) {
super();
}
protected async _call(input: string): Promise<string> {
try {
const inputFormat = JSON.parse(input);
const proposalIndex = inputFormat.proposalIndex ?? undefined;
const tx = await this.solanaKit.approveMultisigProposal(proposalIndex);
return JSON.stringify({
status: "success",
message: "Proposal approved successfully",
transaction: tx,
proposalIndex: proposalIndex.toString(),
});
} catch (error: any) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "APPROVE_PROPOSAL_2BY2_MULTISIG_ERROR",
});
}
}
}
export class SolanaRejectProposal2by2Multisig extends Tool {
name = "reject_proposal_2by2_multisig";
description = `Reject a proposal to transfer funds from a 2-of-2 multisig account on Solana with the user and the agent, where both approvals will be required to run the transactions.
If proposalIndex is not provided, the latest index will automatically be fetched and used.
Inputs (JSON string):
- proposalIndex: number, the index of the proposal (optional).`;
constructor(private solanaKit: SolanaAgentKit) {
super();
}
protected async _call(input: string): Promise<string> {
try {
const inputFormat = JSON.parse(input);
const proposalIndex = inputFormat.proposalIndex ?? undefined;
const tx = await this.solanaKit.rejectMultisigProposal(proposalIndex);
return JSON.stringify({
status: "success",
message: "Proposal rejected successfully",
transaction: tx,
proposalIndex: proposalIndex.toString(),
});
} catch (error: any) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "REJECT_PROPOSAL_2BY2_MULTISIG_ERROR",
});
}
}
}
export class SolanaExecuteProposal2by2Multisig extends Tool {
name = "execute_proposal_2by2_multisig";
description = `Execute a proposal/transaction to transfer funds from a 2-of-2 multisig account on Solana with the user and the agent, where both approvals will be required to run the transactions.
If proposalIndex is not provided, the latest index will automatically be fetched and used.
Inputs (JSON string):
- proposalIndex: number, the index of the proposal (optional).`;
constructor(private solanaKit: SolanaAgentKit) {
super();
}
protected async _call(input: string): Promise<string> {
try {
const inputFormat = JSON.parse(input);
const proposalIndex = inputFormat.proposalIndex ?? undefined;
const tx = await this.solanaKit.executeMultisigTransaction(proposalIndex);
return JSON.stringify({
status: "success",
message: "Proposal executed successfully",
transaction: tx,
proposalIndex: proposalIndex.toString(),
});
} catch (error: any) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "EXECUTE_PROPOSAL_2BY2_MULTISIG_ERROR",
});
}
}
}
export function createSolanaTools(solanaKit: SolanaAgentKit) {
return [
new SolanaBalanceTool(solanaKit),
@@ -2495,5 +2748,12 @@ export function createSolanaTools(solanaKit: SolanaAgentKit) {
new SolanaFlashOpenTrade(solanaKit),
new SolanaFlashCloseTrade(solanaKit),
new Solana3LandCreateSingle(solanaKit),
new SolanaCreate2by2Multisig(solanaKit),
new SolanaDepositTo2by2Multisig(solanaKit),
new SolanaTransferFrom2by2Multisig(solanaKit),
new SolanaCreateProposal2by2Multisig(solanaKit),
new SolanaApproveProposal2by2Multisig(solanaKit),
new SolanaRejectProposal2by2Multisig(solanaKit),
new SolanaExecuteProposal2by2Multisig(solanaKit),
];
}

View File

@@ -0,0 +1,52 @@
import { SolanaAgentKit } from "../../index";
import * as multisig from "@sqds/multisig";
const { Multisig } = multisig.accounts;
/**
* Approves a proposal in a Solana multisig wallet.
*
* @param {SolanaAgentKit} agent - The Solana agent kit instance.
* @param {number | bigint} [transactionIndex] - The index of the transaction to approve. If not provided, the current transaction index will be used.
* @returns {Promise<string>} - A promise that resolves to the transaction ID of the approved proposal.
* @throws {Error} - Throws an error if the approval process fails.
*/
export async function approve_proposal(
agent: SolanaAgentKit,
transactionIndex?: number | bigint,
): Promise<string> {
try {
const createKey = agent.wallet;
const [multisigPda] = multisig.getMultisigPda({
createKey: createKey.publicKey,
});
const multisigInfo = await Multisig.fromAccountAddress(
agent.connection,
multisigPda,
);
const currentTransactionIndex = Number(multisigInfo.transactionIndex);
if (!transactionIndex) {
transactionIndex = BigInt(currentTransactionIndex);
} else if (typeof transactionIndex !== "bigint") {
transactionIndex = BigInt(transactionIndex);
}
// const [proposalPda, proposalBump] = multisig.getProposalPda({
// multisigPda,
// transactionIndex,
// });
const multisigTx = multisig.transactions.proposalApprove({
blockhash: (await agent.connection.getLatestBlockhash()).blockhash,
feePayer: agent.wallet.publicKey,
multisigPda,
transactionIndex: transactionIndex,
member: agent.wallet.publicKey,
});
multisigTx.sign([agent.wallet]);
const tx = await agent.connection.sendRawTransaction(
multisigTx.serialize(),
);
return tx;
} catch (error: any) {
throw new Error(`Transfer failed: ${error}`);
}
}

View File

@@ -0,0 +1,62 @@
import * as multisig from "@sqds/multisig";
import { PublicKey } from "@solana/web3.js";
import { SolanaAgentKit } from "../../index";
/**
* Creates a new Squads multisig account.
*
* @param agent - The SolanaAgentKit instance containing the connection and wallet information.
* @param creator - The public key of the creator who will be a member of the multisig.
* @returns A promise that resolves to the transaction ID of the multisig creation transaction.
*
* @throws Will throw an error if the transaction fails.
*/
export async function create_squads_multisig(
agent: SolanaAgentKit,
creator: PublicKey,
): Promise<string> {
const connection = agent.connection;
const createKey = agent.wallet; // can be any keypair, using the agent wallet as only one multisig is required
console.log("Multisig Create Key:", createKey.publicKey.toBase58());
const [multisigPda] = multisig.getMultisigPda({
createKey: createKey.publicKey,
});
const programConfigPda = multisig.getProgramConfigPda({})[0];
const programConfig =
await multisig.accounts.ProgramConfig.fromAccountAddress(
connection,
programConfigPda,
);
const configTreasury = programConfig.treasury;
const tx = multisig.transactions.multisigCreateV2({
blockhash: (await connection.getLatestBlockhash()).blockhash,
treasury: configTreasury,
createKey: createKey.publicKey,
creator: agent.wallet.publicKey,
multisigPda,
configAuthority: null,
timeLock: 0,
threshold: 2,
rentCollector: null,
members: [
{
key: agent.wallet.publicKey,
permissions: multisig.types.Permissions.all(),
},
{
key: creator,
permissions: multisig.types.Permissions.all(),
},
],
});
tx.sign([agent.wallet, createKey]);
const txId = connection.sendRawTransaction(tx.serialize());
return txId;
}

View File

@@ -0,0 +1,48 @@
import { SolanaAgentKit } from "../../index";
import * as multisig from "@sqds/multisig";
const { Multisig } = multisig.accounts;
/**
* Creates a proposal for a multisig transaction.
*
* @param {SolanaAgentKit} agent - The Solana agent kit instance.
* @param {number | bigint} [transactionIndex] - Optional transaction index. If not provided, the current transaction index will be used.
* @returns {Promise<string>} - The transaction ID of the created proposal.
* @throws {Error} - Throws an error if the proposal creation fails.
*/
export async function create_proposal(
agent: SolanaAgentKit,
transactionIndex?: number | bigint,
): Promise<string> {
try {
const createKey = agent.wallet;
const [multisigPda] = multisig.getMultisigPda({
createKey: createKey.publicKey,
});
const multisigInfo = await Multisig.fromAccountAddress(
agent.connection,
multisigPda,
);
const currentTransactionIndex = Number(multisigInfo.transactionIndex);
if (!transactionIndex) {
transactionIndex = BigInt(currentTransactionIndex);
} else if (typeof transactionIndex !== "bigint") {
transactionIndex = BigInt(transactionIndex);
}
const multisigTx = multisig.transactions.proposalCreate({
blockhash: (await agent.connection.getLatestBlockhash()).blockhash,
feePayer: agent.wallet_address,
multisigPda,
transactionIndex,
creator: agent.wallet_address,
});
multisigTx.sign([agent.wallet]);
const tx = await agent.connection.sendRawTransaction(
multisigTx.serialize(),
);
return tx;
} catch (error: any) {
throw new Error(`Transfer failed: ${error}`);
}
}

View File

@@ -0,0 +1,91 @@
import { SolanaAgentKit } from "../../index";
import { PublicKey, SystemProgram, Transaction } from "@solana/web3.js";
import { LAMPORTS_PER_SOL } from "@solana/web3.js";
import {
getAssociatedTokenAddress,
createTransferInstruction,
getMint,
createAssociatedTokenAccountInstruction,
} from "@solana/spl-token";
import * as multisig from "@sqds/multisig";
/**
* Transfer SOL or SPL tokens to a multisig vault.
* @param agent SolanaAgentKit instance
* @param amount Amount to transfer
* @param vaultIndex Optional vault index, default is 0
* @param mint Optional mint address for SPL tokens
* @returns Transaction signature
*/
export async function deposit_to_multisig(
agent: SolanaAgentKit,
amount: number,
vaultIndex?: number,
mint?: PublicKey,
): Promise<string> {
try {
let tx: string;
if (!vaultIndex) {
vaultIndex = 0;
}
const createKey = agent.wallet;
const [multisigPda] = multisig.getMultisigPda({
createKey: createKey.publicKey,
});
const [vaultPda] = multisig.getVaultPda({
multisigPda,
index: vaultIndex,
});
const to = vaultPda;
if (!mint) {
// Transfer native SOL
const transaction = new Transaction().add(
SystemProgram.transfer({
fromPubkey: agent.wallet_address,
toPubkey: to,
lamports: amount * LAMPORTS_PER_SOL,
}),
);
tx = await agent.connection.sendTransaction(transaction, [agent.wallet]);
} else {
// Transfer SPL token
const fromAta = await getAssociatedTokenAddress(
mint,
agent.wallet_address,
);
let transaction = new Transaction();
const toAta = await getAssociatedTokenAddress(mint, to, true);
const toTokenAccountInfo = await agent.connection.getAccountInfo(toAta);
// Create associated token account if it doesn't exist
if (!toTokenAccountInfo) {
transaction.add(
createAssociatedTokenAccountInstruction(
agent.wallet_address,
toAta,
to,
mint,
),
);
}
// Get mint info to determine decimals
const mintInfo = await getMint(agent.connection, mint);
const adjustedAmount = amount * Math.pow(10, mintInfo.decimals);
transaction.add(
createTransferInstruction(
fromAta,
toAta,
agent.wallet_address,
adjustedAmount,
),
);
tx = await agent.connection.sendTransaction(transaction, [agent.wallet]);
}
return tx;
} catch (error: any) {
throw new Error(`Transfer failed: ${error}`);
}
}

View File

@@ -0,0 +1,49 @@
import { SolanaAgentKit } from "../../index";
import * as multisig from "@sqds/multisig";
const { Multisig } = multisig.accounts;
/**
* Executes a transaction on the Solana blockchain using the provided agent.
*
* @param {SolanaAgentKit} agent - The Solana agent kit instance containing the wallet and connection.
* @param {number | bigint} [transactionIndex] - Optional transaction index to execute. If not provided, the current transaction index from the multisig account will be used.
* @returns {Promise<string>} - A promise that resolves to the transaction signature string.
* @throws {Error} - Throws an error if the transaction execution fails.
*/
export async function execute_transaction(
agent: SolanaAgentKit,
transactionIndex?: number | bigint,
): Promise<string> {
try {
const createKey = agent.wallet;
const [multisigPda] = multisig.getMultisigPda({
createKey: createKey.publicKey,
});
const multisigInfo = await Multisig.fromAccountAddress(
agent.connection,
multisigPda,
);
const currentTransactionIndex = Number(multisigInfo.transactionIndex);
if (!transactionIndex) {
transactionIndex = BigInt(currentTransactionIndex);
} else if (typeof transactionIndex !== "bigint") {
transactionIndex = BigInt(transactionIndex);
}
const multisigTx = await multisig.transactions.vaultTransactionExecute({
connection: agent.connection,
blockhash: (await agent.connection.getLatestBlockhash()).blockhash,
feePayer: agent.wallet.publicKey,
multisigPda,
transactionIndex,
member: agent.wallet.publicKey,
});
multisigTx.sign([agent.wallet]);
const tx = await agent.connection.sendRawTransaction(
multisigTx.serialize(),
);
return tx;
} catch (error: any) {
throw new Error(`Transfer failed: ${error}`);
}
}

View File

@@ -0,0 +1,52 @@
import { SolanaAgentKit } from "../../index";
import * as multisig from "@sqds/multisig";
const { Multisig } = multisig.accounts;
/**
* Rejects a proposal in a Solana multisig setup.
*
* @param agent - The SolanaAgentKit instance containing the wallet and connection.
* @param transactionIndex - Optional. The index of the transaction to reject. If not provided, the current transaction index will be used.
* @returns A promise that resolves to the transaction ID of the rejection transaction.
* @throws Will throw an error if the transaction fails.
*/
export async function reject_proposal(
agent: SolanaAgentKit,
transactionIndex?: number | bigint,
): Promise<string> {
try {
const createKey = agent.wallet;
const [multisigPda] = multisig.getMultisigPda({
createKey: createKey.publicKey,
});
const multisigInfo = await Multisig.fromAccountAddress(
agent.connection,
multisigPda,
);
const currentTransactionIndex = Number(multisigInfo.transactionIndex);
if (!transactionIndex) {
transactionIndex = BigInt(currentTransactionIndex);
} else if (typeof transactionIndex !== "bigint") {
transactionIndex = BigInt(transactionIndex);
}
// const [proposalPda, proposalBump] = multisig.getProposalPda({
// multisigPda,
// transactionIndex,
// });
const multisigTx = multisig.transactions.proposalReject({
blockhash: (await agent.connection.getLatestBlockhash()).blockhash,
feePayer: agent.wallet.publicKey,
multisigPda,
transactionIndex: transactionIndex,
member: agent.wallet.publicKey,
});
multisigTx.sign([agent.wallet]);
const tx = await agent.connection.sendRawTransaction(
multisigTx.serialize(),
);
return tx;
} catch (error: any) {
throw new Error(`Transfer failed: ${error}`);
}
}

View File

@@ -0,0 +1,98 @@
import { SolanaAgentKit } from "../../index";
import {
PublicKey,
SystemProgram,
TransactionInstruction,
TransactionMessage,
} from "@solana/web3.js";
import { LAMPORTS_PER_SOL } from "@solana/web3.js";
import {
getAssociatedTokenAddress,
createTransferInstruction,
getMint,
} from "@solana/spl-token";
import * as multisig from "@sqds/multisig";
const { Multisig } = multisig.accounts;
/**
* Transfer SOL or SPL tokens to a recipient from a multisig vault.
* @param agent - SolanaAgentKit instance.
* @param amount - Amount to transfer.
* @param to - Recipient's public key.
* @param vaultIndex - Optional vault index, default is 0.
* @param mint - Optional mint address for SPL tokens.
* @returns Transaction signature.
*/
export async function transfer_from_multisig(
agent: SolanaAgentKit,
amount: number,
to: PublicKey,
vaultIndex: number = 0,
mint?: PublicKey,
): Promise<string> {
try {
let transferInstruction: TransactionInstruction;
const createKey = agent.wallet;
const [multisigPda] = multisig.getMultisigPda({
createKey: createKey.publicKey,
});
const multisigInfo = await Multisig.fromAccountAddress(
agent.connection,
multisigPda,
);
const currentTransactionIndex = Number(multisigInfo.transactionIndex);
const transactionIndex = BigInt(currentTransactionIndex + 1);
const [vaultPda] = multisig.getVaultPda({
multisigPda,
index: vaultIndex,
});
if (!mint) {
// Transfer native SOL
transferInstruction = SystemProgram.transfer({
fromPubkey: agent.wallet_address,
toPubkey: to,
lamports: amount * LAMPORTS_PER_SOL,
});
} else {
// Transfer SPL token
const fromAta = await getAssociatedTokenAddress(mint, vaultPda, true);
const toAta = await getAssociatedTokenAddress(mint, to, true);
const mintInfo = await getMint(agent.connection, mint);
const adjustedAmount = amount * Math.pow(10, mintInfo.decimals);
transferInstruction = createTransferInstruction(
fromAta,
toAta,
agent.wallet_address,
adjustedAmount,
);
}
const transferMessage = new TransactionMessage({
payerKey: vaultPda,
recentBlockhash: (await agent.connection.getLatestBlockhash()).blockhash,
instructions: [transferInstruction],
});
const multisigTx = multisig.transactions.vaultTransactionCreate({
blockhash: (await agent.connection.getLatestBlockhash()).blockhash,
feePayer: agent.wallet_address,
multisigPda,
transactionIndex,
creator: agent.wallet_address,
vaultIndex: 0,
ephemeralSigners: 0,
transactionMessage: transferMessage,
});
multisigTx.sign([agent.wallet]);
const tx = await agent.connection.sendRawTransaction(
multisigTx.serialize(),
);
return tx;
} catch (error: any) {
throw new Error(`Transfer failed: ${error}`);
}
}