Skip to content

Commit

Permalink
adding 2/2 multisig \w agent & user (#88)
Browse files Browse the repository at this point in the history
# 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
  • Loading branch information
thearyanag authored Jan 9, 2025
2 parents 6c66606 + 5ba7178 commit fc92d44
Show file tree
Hide file tree
Showing 13 changed files with 1,137 additions and 354 deletions.
27 changes: 14 additions & 13 deletions docs/classes/SolanaAgentKit.html

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions docs/interfaces/JupiterTokenData.html

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
695 changes: 354 additions & 341 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

52 changes: 52 additions & 0 deletions src/agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
}
260 changes: 260 additions & 0 deletions src/langchain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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),
];
}
52 changes: 52 additions & 0 deletions src/tools/squads_multisig/approve_proposal.ts
Original file line number Diff line number Diff line change
@@ -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}`);
}
}
Loading

0 comments on commit fc92d44

Please sign in to comment.