fortis-multisig-client 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generated/accounts/index.d.ts +10 -0
- package/dist/generated/accounts/index.js +10 -0
- package/dist/generated/accounts/multisig.d.ts +51 -0
- package/dist/generated/accounts/multisig.js +52 -0
- package/dist/generated/accounts/proposal.d.ts +49 -0
- package/dist/generated/accounts/proposal.js +54 -0
- package/dist/generated/accounts/vaultTransaction.d.ts +66 -0
- package/dist/generated/accounts/vaultTransaction.js +61 -0
- package/dist/generated/index.d.ts +11 -0
- package/dist/generated/index.js +11 -0
- package/dist/generated/instructions/index.d.ts +12 -0
- package/dist/generated/instructions/index.js +12 -0
- package/dist/generated/instructions/multisigCreate.d.ts +65 -0
- package/dist/generated/instructions/multisigCreate.js +80 -0
- package/dist/generated/instructions/proposalAccountsClose.d.ts +54 -0
- package/dist/generated/instructions/proposalAccountsClose.js +76 -0
- package/dist/generated/instructions/proposalApprove.d.ts +47 -0
- package/dist/generated/instructions/proposalApprove.js +74 -0
- package/dist/generated/instructions/proposalCreate.d.ts +57 -0
- package/dist/generated/instructions/proposalCreate.js +80 -0
- package/dist/generated/instructions/proposalExecute.d.ts +49 -0
- package/dist/generated/instructions/proposalExecute.js +70 -0
- package/dist/generated/programs/fortisMultisig.d.ts +36 -0
- package/dist/generated/programs/fortisMultisig.js +42 -0
- package/dist/generated/programs/index.d.ts +8 -0
- package/dist/generated/programs/index.js +8 -0
- package/dist/generated/shared/index.d.ts +49 -0
- package/dist/generated/shared/index.js +86 -0
- package/dist/generated/types/compiledInstruction.d.ts +20 -0
- package/dist/generated/types/compiledInstruction.js +31 -0
- package/dist/generated/types/index.d.ts +13 -0
- package/dist/generated/types/index.js +13 -0
- package/dist/generated/types/messageAddressTableLookup.d.ts +24 -0
- package/dist/generated/types/messageAddressTableLookup.js +37 -0
- package/dist/generated/types/multisigCreateArgs.d.ts +33 -0
- package/dist/generated/types/multisigCreateArgs.js +25 -0
- package/dist/generated/types/proposalApproveArgs.d.ts +13 -0
- package/dist/generated/types/proposalApproveArgs.js +17 -0
- package/dist/generated/types/proposalCreateArgs.d.ts +25 -0
- package/dist/generated/types/proposalCreateArgs.js +31 -0
- package/dist/generated/types/vaultTransactionMessage.d.ts +68 -0
- package/dist/generated/types/vaultTransactionMessage.js +38 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +7 -0
- package/dist/instructions/index.d.ts +12 -0
- package/dist/instructions/index.js +12 -0
- package/dist/instructions/multisigCreate.d.ts +10 -0
- package/dist/instructions/multisigCreate.js +26 -0
- package/dist/instructions/proposalAccountsClose.d.ts +6 -0
- package/dist/instructions/proposalAccountsClose.js +19 -0
- package/dist/instructions/proposalApprove.d.ts +6 -0
- package/dist/instructions/proposalApprove.js +15 -0
- package/dist/instructions/proposalCreate.d.ts +10 -0
- package/dist/instructions/proposalCreate.js +27 -0
- package/dist/instructions/proposalExecute.d.ts +10 -0
- package/dist/instructions/proposalExecute.js +39 -0
- package/dist/pda.d.ts +27 -0
- package/dist/pda.js +41 -0
- package/dist/utils/compileToWrappedMessageV0.d.ts +11 -0
- package/dist/utils/compileToWrappedMessageV0.js +29 -0
- package/dist/utils/compiled-keys.d.ts +27 -0
- package/dist/utils/compiled-keys.js +112 -0
- package/dist/utils.d.ts +28 -0
- package/dist/utils.js +184 -0
- package/package.json +2 -2
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { SystemProgram } from "@solana/web3.js";
|
|
2
|
+
import { getProposalCreateInstruction } from "../generated";
|
|
3
|
+
import * as pda from "../pda";
|
|
4
|
+
import * as utils from "../utils";
|
|
5
|
+
import { fromLegacyPublicKey } from "@solana/compat";
|
|
6
|
+
export async function proposalCreate({ creator, multisigPda, ephemeralSigners, votingDeadline, transactionMessage, addressLookupTableAccounts, transactionIndex, }) {
|
|
7
|
+
const transactionMessageBytes = await utils.transactionMessageToMultisigTransactionMessageBytes({
|
|
8
|
+
message: transactionMessage,
|
|
9
|
+
addressLookupTableAccounts,
|
|
10
|
+
});
|
|
11
|
+
const args = {
|
|
12
|
+
ephemeralSigners,
|
|
13
|
+
votingDeadline,
|
|
14
|
+
transactionMessage: transactionMessageBytes,
|
|
15
|
+
};
|
|
16
|
+
let txPda = await pda.getTransactionPda({ multisigPda: multisigPda, index: transactionIndex });
|
|
17
|
+
let proposalPda = await pda.getProposalPda({ multisigPda: multisigPda, transactionIndex: transactionIndex });
|
|
18
|
+
let ix = getProposalCreateInstruction({
|
|
19
|
+
multisig: fromLegacyPublicKey(multisigPda),
|
|
20
|
+
transaction: fromLegacyPublicKey(txPda[0]),
|
|
21
|
+
creator: fromLegacyPublicKey(creator),
|
|
22
|
+
proposal: fromLegacyPublicKey(proposalPda[0]),
|
|
23
|
+
systemProgram: fromLegacyPublicKey(SystemProgram.programId),
|
|
24
|
+
args,
|
|
25
|
+
});
|
|
26
|
+
return utils.toLegacyTransactionInstruction(ix, [2]);
|
|
27
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { PublicKey, TransactionInstruction, Connection, AddressLookupTableAccount } from "@solana/web3.js";
|
|
2
|
+
export declare function proposalExecute({ connection, member, multisigPda, transactionIndex, }: {
|
|
3
|
+
connection: Connection;
|
|
4
|
+
member: PublicKey;
|
|
5
|
+
multisigPda: PublicKey;
|
|
6
|
+
transactionIndex: bigint;
|
|
7
|
+
}): Promise<{
|
|
8
|
+
instruction: TransactionInstruction;
|
|
9
|
+
lookupTableAccounts: AddressLookupTableAccount[];
|
|
10
|
+
}>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { PublicKey } from "@solana/web3.js";
|
|
2
|
+
import { getProposalExecuteInstruction, getVaultTransactionCodec } from "../generated";
|
|
3
|
+
import * as pda from "../pda";
|
|
4
|
+
import * as utils from "../utils";
|
|
5
|
+
import { address, AccountRole } from "@solana/kit";
|
|
6
|
+
import { fromLegacyPublicKey } from "@solana/compat";
|
|
7
|
+
export async function proposalExecute({ connection, member, multisigPda, transactionIndex, }) {
|
|
8
|
+
let transactionPda = await pda.getTransactionPda({ multisigPda: multisigPda, index: transactionIndex });
|
|
9
|
+
let proposalPda = await pda.getProposalPda({ multisigPda: multisigPda, transactionIndex: transactionIndex });
|
|
10
|
+
let vaultPda = await pda.getVaultPda({ multisigPda: multisigPda });
|
|
11
|
+
const txAccount = await connection.getAccountInfo(new PublicKey(transactionPda[0]));
|
|
12
|
+
if (!txAccount) {
|
|
13
|
+
throw new Error('Transaction account not found');
|
|
14
|
+
}
|
|
15
|
+
const transactionAccount = getVaultTransactionCodec().decode(txAccount.data);
|
|
16
|
+
const { accountMetas, lookupTableAccounts } = await utils.accountsForTransactionExecute({
|
|
17
|
+
connection,
|
|
18
|
+
message: transactionAccount.message,
|
|
19
|
+
ephemeralSignerBumps: [...transactionAccount.ephemeralSignerBumps],
|
|
20
|
+
vaultPda: vaultPda[0],
|
|
21
|
+
transactionPda: transactionPda[0]
|
|
22
|
+
});
|
|
23
|
+
let ix = getProposalExecuteInstruction({
|
|
24
|
+
multisig: fromLegacyPublicKey(multisigPda),
|
|
25
|
+
proposal: fromLegacyPublicKey(proposalPda[0]),
|
|
26
|
+
transaction: fromLegacyPublicKey(transactionPda[0]),
|
|
27
|
+
member: fromLegacyPublicKey(member),
|
|
28
|
+
});
|
|
29
|
+
// Convert only additional accounts to Kinobi type
|
|
30
|
+
const additionalAccounts = accountMetas.map(am => {
|
|
31
|
+
return {
|
|
32
|
+
address: address(am.pubkey.toBase58()),
|
|
33
|
+
role: am.isWritable ? AccountRole.WRITABLE : AccountRole.READONLY,
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
// Append them
|
|
37
|
+
ix.accounts.push(...additionalAccounts);
|
|
38
|
+
return { instruction: utils.toLegacyTransactionInstruction(ix, [3]), lookupTableAccounts };
|
|
39
|
+
}
|
package/dist/pda.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { PublicKey } from "@solana/web3.js";
|
|
2
|
+
import { Address } from "@solana/kit";
|
|
3
|
+
export declare const FORTIS_TREASURY: Address<"5wBH8hqU4PxVCFXmu3JR6Kegdy2Vq8K7fZnRgN5ZJEr2">;
|
|
4
|
+
export declare function getMultisigPda({ createKey, programId, }: {
|
|
5
|
+
createKey: PublicKey;
|
|
6
|
+
programId?: PublicKey;
|
|
7
|
+
}): [PublicKey, number];
|
|
8
|
+
export declare function getVaultPda({ multisigPda, programId, }: {
|
|
9
|
+
multisigPda: PublicKey;
|
|
10
|
+
programId?: PublicKey;
|
|
11
|
+
}): [PublicKey, number];
|
|
12
|
+
export declare function getEphemeralSignerPda({ transactionPda, ephemeralSignerIndex, programId, }: {
|
|
13
|
+
transactionPda: PublicKey;
|
|
14
|
+
ephemeralSignerIndex: number;
|
|
15
|
+
programId?: PublicKey;
|
|
16
|
+
}): [PublicKey, number];
|
|
17
|
+
export declare function getTransactionPda({ multisigPda, index, programId, }: {
|
|
18
|
+
multisigPda: PublicKey;
|
|
19
|
+
/** Transaction index. */
|
|
20
|
+
index: bigint;
|
|
21
|
+
programId?: PublicKey;
|
|
22
|
+
}): [PublicKey, number];
|
|
23
|
+
export declare function getProposalPda({ multisigPda, transactionIndex, programId, }: {
|
|
24
|
+
multisigPda: PublicKey;
|
|
25
|
+
transactionIndex: bigint;
|
|
26
|
+
programId?: PublicKey;
|
|
27
|
+
}): [PublicKey, number];
|
package/dist/pda.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { FORTIS_MULTISIG_PROGRAM_ADDRESS } from "./generated";
|
|
2
|
+
import { toUtfBytes } from "./utils";
|
|
3
|
+
import { PublicKey } from "@solana/web3.js";
|
|
4
|
+
import { address, getU64Encoder } from "@solana/kit";
|
|
5
|
+
const SEED_PREFIX = toUtfBytes("multisig");
|
|
6
|
+
const SEED_MULTISIG = toUtfBytes("multisig");
|
|
7
|
+
const SEED_VAULT = toUtfBytes("vault");
|
|
8
|
+
const SEED_TRANSACTION = toUtfBytes("transaction");
|
|
9
|
+
const SEED_PROPOSAL = toUtfBytes("proposal");
|
|
10
|
+
const SEED_EPHEMERAL_SIGNER = toUtfBytes("ephemeral_signer");
|
|
11
|
+
const prgramId = FORTIS_MULTISIG_PROGRAM_ADDRESS;
|
|
12
|
+
export const FORTIS_TREASURY = address("5wBH8hqU4PxVCFXmu3JR6Kegdy2Vq8K7fZnRgN5ZJEr2");
|
|
13
|
+
export function getMultisigPda({ createKey, programId = new PublicKey(FORTIS_MULTISIG_PROGRAM_ADDRESS), }) {
|
|
14
|
+
return PublicKey.findProgramAddressSync([SEED_PREFIX, SEED_MULTISIG, createKey.toBytes()], programId);
|
|
15
|
+
}
|
|
16
|
+
export function getVaultPda({ multisigPda, programId = new PublicKey(FORTIS_MULTISIG_PROGRAM_ADDRESS), }) {
|
|
17
|
+
return PublicKey.findProgramAddressSync([SEED_PREFIX, multisigPda.toBytes(), SEED_VAULT], programId);
|
|
18
|
+
}
|
|
19
|
+
export function getEphemeralSignerPda({ transactionPda, ephemeralSignerIndex, programId = new PublicKey(FORTIS_MULTISIG_PROGRAM_ADDRESS), }) {
|
|
20
|
+
const buf = new Uint8Array([ephemeralSignerIndex]); // ✅ works
|
|
21
|
+
return PublicKey.findProgramAddressSync([
|
|
22
|
+
SEED_PREFIX,
|
|
23
|
+
transactionPda.toBytes(),
|
|
24
|
+
SEED_EPHEMERAL_SIGNER,
|
|
25
|
+
buf,
|
|
26
|
+
], programId);
|
|
27
|
+
}
|
|
28
|
+
export function getTransactionPda({ multisigPda, index, programId = new PublicKey(FORTIS_MULTISIG_PROGRAM_ADDRESS), }) {
|
|
29
|
+
const buf = Uint8Array.from(getU64Encoder().encode(index));
|
|
30
|
+
return PublicKey.findProgramAddressSync([SEED_PREFIX, multisigPda.toBytes(), SEED_TRANSACTION, buf], programId);
|
|
31
|
+
}
|
|
32
|
+
export function getProposalPda({ multisigPda, transactionIndex, programId = new PublicKey(FORTIS_MULTISIG_PROGRAM_ADDRESS), }) {
|
|
33
|
+
const buf = Uint8Array.from(getU64Encoder().encode(transactionIndex));
|
|
34
|
+
return PublicKey.findProgramAddressSync([
|
|
35
|
+
SEED_PREFIX,
|
|
36
|
+
multisigPda.toBytes(),
|
|
37
|
+
SEED_TRANSACTION,
|
|
38
|
+
buf,
|
|
39
|
+
SEED_PROPOSAL,
|
|
40
|
+
], programId);
|
|
41
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AddressLookupTableAccount, MessageAddressTableLookup, PublicKey, MessageCompiledInstruction, TransactionInstruction } from "@solana/web3.js";
|
|
2
|
+
export declare function compileToWrappedMessageV0({ payerKey, instructions, addressLookupTableAccounts, }: {
|
|
3
|
+
payerKey: PublicKey;
|
|
4
|
+
instructions: TransactionInstruction[];
|
|
5
|
+
addressLookupTableAccounts?: AddressLookupTableAccount[];
|
|
6
|
+
}): {
|
|
7
|
+
header: import("@solana/web3.js").MessageHeader;
|
|
8
|
+
staticAccountKeys: PublicKey[];
|
|
9
|
+
compiledInstructions: MessageCompiledInstruction[];
|
|
10
|
+
addressTableLookups: MessageAddressTableLookup[];
|
|
11
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { MessageAccountKeys, } from "@solana/web3.js";
|
|
2
|
+
import { CompiledKeys } from "./compiled-keys";
|
|
3
|
+
export function compileToWrappedMessageV0({ payerKey, instructions, addressLookupTableAccounts, }) {
|
|
4
|
+
const compiledKeys = CompiledKeys.compile(instructions, payerKey);
|
|
5
|
+
const addressTableLookups = new Array();
|
|
6
|
+
const accountKeysFromLookups = {
|
|
7
|
+
writable: [],
|
|
8
|
+
readonly: [],
|
|
9
|
+
};
|
|
10
|
+
const lookupTableAccounts = addressLookupTableAccounts || [];
|
|
11
|
+
for (const lookupTable of lookupTableAccounts) {
|
|
12
|
+
const extractResult = compiledKeys.extractTableLookup(lookupTable);
|
|
13
|
+
if (extractResult !== undefined) {
|
|
14
|
+
const [addressTableLookup, { writable, readonly }] = extractResult;
|
|
15
|
+
addressTableLookups.push(addressTableLookup);
|
|
16
|
+
accountKeysFromLookups.writable.push(...writable);
|
|
17
|
+
accountKeysFromLookups.readonly.push(...readonly);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const [header, staticAccountKeys] = compiledKeys.getMessageComponents();
|
|
21
|
+
const accountKeys = new MessageAccountKeys(staticAccountKeys, accountKeysFromLookups);
|
|
22
|
+
const compiledInstructions = accountKeys.compileInstructions(instructions);
|
|
23
|
+
return {
|
|
24
|
+
header,
|
|
25
|
+
staticAccountKeys,
|
|
26
|
+
compiledInstructions,
|
|
27
|
+
addressTableLookups,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { MessageHeader, MessageAddressTableLookup, AccountKeysFromLookups, AddressLookupTableAccount, TransactionInstruction, PublicKey } from "@solana/web3.js";
|
|
2
|
+
export type CompiledKeyMeta = {
|
|
3
|
+
isSigner: boolean;
|
|
4
|
+
isWritable: boolean;
|
|
5
|
+
isInvoked: boolean;
|
|
6
|
+
};
|
|
7
|
+
type KeyMetaMap = Map<string, CompiledKeyMeta>;
|
|
8
|
+
/**
|
|
9
|
+
* This is almost completely copy-pasted from solana-web3.js and slightly adapted to work with "wrapped" transaction messaged such as in VaultTransaction.
|
|
10
|
+
* @see https://github.com/solana-labs/solana-web3.js/blob/87d33ac68e2453b8a01cf8c425aa7623888434e8/packages/library-legacy/src/message/compiled-keys.ts
|
|
11
|
+
*/
|
|
12
|
+
export declare class CompiledKeys {
|
|
13
|
+
payer: PublicKey;
|
|
14
|
+
keyMetaMap: KeyMetaMap;
|
|
15
|
+
constructor(payer: PublicKey, keyMetaMap: KeyMetaMap);
|
|
16
|
+
/**
|
|
17
|
+
* The only difference between this and the original is that we don't mark the instruction programIds as invoked.
|
|
18
|
+
* It makes sense to do because the instructions will be called via CPI, so the programIds can come from Address Lookup Tables.
|
|
19
|
+
* This allows to compress the message size and avoid hitting the tx size limit during vault_transaction_create instruction calls.
|
|
20
|
+
*/
|
|
21
|
+
static compile(instructions: Array<TransactionInstruction>, payer: PublicKey): CompiledKeys;
|
|
22
|
+
getMessageComponents(): [MessageHeader, Array<PublicKey>];
|
|
23
|
+
extractTableLookup(lookupTable: AddressLookupTableAccount): [MessageAddressTableLookup, AccountKeysFromLookups] | undefined;
|
|
24
|
+
/** @internal */
|
|
25
|
+
private drainKeysFoundInLookupTable;
|
|
26
|
+
}
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import assert from "assert";
|
|
2
|
+
import { PublicKey, } from "@solana/web3.js";
|
|
3
|
+
/**
|
|
4
|
+
* This is almost completely copy-pasted from solana-web3.js and slightly adapted to work with "wrapped" transaction messaged such as in VaultTransaction.
|
|
5
|
+
* @see https://github.com/solana-labs/solana-web3.js/blob/87d33ac68e2453b8a01cf8c425aa7623888434e8/packages/library-legacy/src/message/compiled-keys.ts
|
|
6
|
+
*/
|
|
7
|
+
export class CompiledKeys {
|
|
8
|
+
payer;
|
|
9
|
+
keyMetaMap;
|
|
10
|
+
constructor(payer, keyMetaMap) {
|
|
11
|
+
this.payer = payer;
|
|
12
|
+
this.keyMetaMap = keyMetaMap;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The only difference between this and the original is that we don't mark the instruction programIds as invoked.
|
|
16
|
+
* It makes sense to do because the instructions will be called via CPI, so the programIds can come from Address Lookup Tables.
|
|
17
|
+
* This allows to compress the message size and avoid hitting the tx size limit during vault_transaction_create instruction calls.
|
|
18
|
+
*/
|
|
19
|
+
static compile(instructions, payer) {
|
|
20
|
+
const keyMetaMap = new Map();
|
|
21
|
+
const getOrInsertDefault = (pubkey) => {
|
|
22
|
+
const address = pubkey.toBase58();
|
|
23
|
+
let keyMeta = keyMetaMap.get(address);
|
|
24
|
+
if (keyMeta === undefined) {
|
|
25
|
+
keyMeta = {
|
|
26
|
+
isSigner: false,
|
|
27
|
+
isWritable: false,
|
|
28
|
+
isInvoked: false,
|
|
29
|
+
};
|
|
30
|
+
keyMetaMap.set(address, keyMeta);
|
|
31
|
+
}
|
|
32
|
+
return keyMeta;
|
|
33
|
+
};
|
|
34
|
+
const payerKeyMeta = getOrInsertDefault(payer);
|
|
35
|
+
payerKeyMeta.isSigner = true;
|
|
36
|
+
payerKeyMeta.isWritable = true;
|
|
37
|
+
for (const ix of instructions) {
|
|
38
|
+
// This is the only difference from the original.
|
|
39
|
+
// getOrInsertDefault(ix.programId).isInvoked = true;
|
|
40
|
+
getOrInsertDefault(ix.programId).isInvoked = false;
|
|
41
|
+
for (const accountMeta of ix.keys) {
|
|
42
|
+
const keyMeta = getOrInsertDefault(accountMeta.pubkey);
|
|
43
|
+
keyMeta.isSigner ||= accountMeta.isSigner;
|
|
44
|
+
keyMeta.isWritable ||= accountMeta.isWritable;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return new CompiledKeys(payer, keyMetaMap);
|
|
48
|
+
}
|
|
49
|
+
getMessageComponents() {
|
|
50
|
+
const mapEntries = [...this.keyMetaMap.entries()];
|
|
51
|
+
assert(mapEntries.length <= 256, "Max static account keys length exceeded");
|
|
52
|
+
const writableSigners = mapEntries.filter(([, meta]) => meta.isSigner && meta.isWritable);
|
|
53
|
+
const readonlySigners = mapEntries.filter(([, meta]) => meta.isSigner && !meta.isWritable);
|
|
54
|
+
const writableNonSigners = mapEntries.filter(([, meta]) => !meta.isSigner && meta.isWritable);
|
|
55
|
+
const readonlyNonSigners = mapEntries.filter(([, meta]) => !meta.isSigner && !meta.isWritable);
|
|
56
|
+
const header = {
|
|
57
|
+
numRequiredSignatures: writableSigners.length + readonlySigners.length,
|
|
58
|
+
numReadonlySignedAccounts: readonlySigners.length,
|
|
59
|
+
numReadonlyUnsignedAccounts: readonlyNonSigners.length,
|
|
60
|
+
};
|
|
61
|
+
// sanity checks
|
|
62
|
+
{
|
|
63
|
+
assert(writableSigners.length > 0, "Expected at least one writable signer key");
|
|
64
|
+
const [payerAddress] = writableSigners[0];
|
|
65
|
+
assert(payerAddress === this.payer.toBase58(), "Expected first writable signer key to be the fee payer");
|
|
66
|
+
}
|
|
67
|
+
const staticAccountKeys = [
|
|
68
|
+
...writableSigners.map(([address]) => new PublicKey(address)),
|
|
69
|
+
...readonlySigners.map(([address]) => new PublicKey(address)),
|
|
70
|
+
...writableNonSigners.map(([address]) => new PublicKey(address)),
|
|
71
|
+
...readonlyNonSigners.map(([address]) => new PublicKey(address)),
|
|
72
|
+
];
|
|
73
|
+
return [header, staticAccountKeys];
|
|
74
|
+
}
|
|
75
|
+
extractTableLookup(lookupTable) {
|
|
76
|
+
const [writableIndexes, drainedWritableKeys] = this.drainKeysFoundInLookupTable(lookupTable.state.addresses, (keyMeta) => !keyMeta.isSigner && !keyMeta.isInvoked && keyMeta.isWritable);
|
|
77
|
+
const [readonlyIndexes, drainedReadonlyKeys] = this.drainKeysFoundInLookupTable(lookupTable.state.addresses, (keyMeta) => !keyMeta.isSigner && !keyMeta.isInvoked && !keyMeta.isWritable);
|
|
78
|
+
// Don't extract lookup if no keys were found
|
|
79
|
+
if (writableIndexes.length === 0 && readonlyIndexes.length === 0) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
return [
|
|
83
|
+
{
|
|
84
|
+
accountKey: lookupTable.key,
|
|
85
|
+
writableIndexes,
|
|
86
|
+
readonlyIndexes,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
writable: drainedWritableKeys,
|
|
90
|
+
readonly: drainedReadonlyKeys,
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
/** @internal */
|
|
95
|
+
drainKeysFoundInLookupTable(lookupTableEntries, keyMetaFilter) {
|
|
96
|
+
const lookupTableIndexes = new Array();
|
|
97
|
+
const drainedKeys = new Array();
|
|
98
|
+
for (const [address, keyMeta] of this.keyMetaMap.entries()) {
|
|
99
|
+
if (keyMetaFilter(keyMeta)) {
|
|
100
|
+
const key = new PublicKey(address);
|
|
101
|
+
const lookupTableIndex = lookupTableEntries.findIndex((entry) => entry.equals(key));
|
|
102
|
+
if (lookupTableIndex >= 0) {
|
|
103
|
+
assert(lookupTableIndex < 256, "Max lookup table index exceeded");
|
|
104
|
+
lookupTableIndexes.push(lookupTableIndex);
|
|
105
|
+
drainedKeys.push(key);
|
|
106
|
+
this.keyMetaMap.delete(address);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return [lookupTableIndexes, drainedKeys];
|
|
111
|
+
}
|
|
112
|
+
}
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { VaultTransactionMessage } from "./generated";
|
|
2
|
+
import { AccountMeta, AddressLookupTableAccount, Connection, PublicKey, VersionedTransaction, TransactionInstruction, TransactionMessage } from "@solana/web3.js";
|
|
3
|
+
import { ReadonlyUint8Array, Instruction } from "@solana/kit";
|
|
4
|
+
export declare function toUtfBytes(str: string): Uint8Array;
|
|
5
|
+
export declare function getAvailableMemoSize(txWithoutMemo: VersionedTransaction): number;
|
|
6
|
+
export declare function isStaticWritableIndex(message: VaultTransactionMessage, index: number): boolean;
|
|
7
|
+
export declare function isSignerIndex(message: VaultTransactionMessage, index: number): boolean;
|
|
8
|
+
/** We use custom serialization for `transaction_message` that ensures as small byte size as possible. */
|
|
9
|
+
export declare function transactionMessageToMultisigTransactionMessageBytes({ message, addressLookupTableAccounts, }: {
|
|
10
|
+
message: TransactionMessage;
|
|
11
|
+
addressLookupTableAccounts: AddressLookupTableAccount[];
|
|
12
|
+
}): Promise<ReadonlyUint8Array>;
|
|
13
|
+
export declare function toLegacyTransactionInstruction(instruction: Instruction, signerAccountIndexes: number[]): TransactionInstruction;
|
|
14
|
+
/** Populate remaining accounts required for execution of the transaction. */
|
|
15
|
+
export declare function accountsForTransactionExecute({ connection, transactionPda, vaultPda, message, ephemeralSignerBumps, programId, addressLookupTableAccounts: localAddressLookupTableAccounts, }: {
|
|
16
|
+
connection: Connection;
|
|
17
|
+
message: VaultTransactionMessage;
|
|
18
|
+
ephemeralSignerBumps: number[];
|
|
19
|
+
vaultPda: PublicKey;
|
|
20
|
+
transactionPda: PublicKey;
|
|
21
|
+
programId?: PublicKey;
|
|
22
|
+
addressLookupTableAccounts?: AddressLookupTableAccount[];
|
|
23
|
+
}): Promise<{
|
|
24
|
+
/** Account metas used in the `message`. */
|
|
25
|
+
accountMetas: AccountMeta[];
|
|
26
|
+
/** Address lookup table accounts used in the `message`. */
|
|
27
|
+
lookupTableAccounts: AddressLookupTableAccount[];
|
|
28
|
+
}>;
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { fromLegacyPublicKey } from '@solana/compat';
|
|
2
|
+
import { Buffer } from "buffer";
|
|
3
|
+
import { getVaultTransactionMessageEncoder } from "./generated";
|
|
4
|
+
import { PublicKey, TransactionInstruction, } from "@solana/web3.js";
|
|
5
|
+
import { getEphemeralSignerPda } from "./pda";
|
|
6
|
+
import invariant from "invariant";
|
|
7
|
+
import { compileToWrappedMessageV0 } from "./utils/compileToWrappedMessageV0";
|
|
8
|
+
import { AccountRole } from "@solana/kit";
|
|
9
|
+
export function toUtfBytes(str) {
|
|
10
|
+
return new TextEncoder().encode(str);
|
|
11
|
+
}
|
|
12
|
+
const MAX_TX_SIZE_BYTES = 1232;
|
|
13
|
+
const STRING_LEN_SIZE = 4;
|
|
14
|
+
export function getAvailableMemoSize(txWithoutMemo) {
|
|
15
|
+
const txSize = txWithoutMemo.serialize().length;
|
|
16
|
+
return (MAX_TX_SIZE_BYTES -
|
|
17
|
+
txSize -
|
|
18
|
+
STRING_LEN_SIZE -
|
|
19
|
+
// Sometimes long memo can trigger switching from 1 to 2 bytes length encoding in Compact-u16,
|
|
20
|
+
// so we reserve 1 extra byte to make sure.
|
|
21
|
+
1);
|
|
22
|
+
}
|
|
23
|
+
export function isStaticWritableIndex(message, index) {
|
|
24
|
+
const numAccountKeys = message.accountKeys.length;
|
|
25
|
+
const { numSigners, numWritableSigners, numWritableNonSigners } = message;
|
|
26
|
+
if (index >= numAccountKeys) {
|
|
27
|
+
// `index` is not a part of static `accountKeys`.
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
if (index < numWritableSigners) {
|
|
31
|
+
// `index` is within the range of writable signer keys.
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
if (index >= numSigners) {
|
|
35
|
+
// `index` is within the range of non-signer keys.
|
|
36
|
+
const indexIntoNonSigners = index - numSigners;
|
|
37
|
+
// Whether `index` is within the range of writable non-signer keys.
|
|
38
|
+
return indexIntoNonSigners < numWritableNonSigners;
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
export function isSignerIndex(message, index) {
|
|
43
|
+
return index < message.numSigners;
|
|
44
|
+
}
|
|
45
|
+
/** We use custom serialization for `transaction_message` that ensures as small byte size as possible. */
|
|
46
|
+
export async function transactionMessageToMultisigTransactionMessageBytes({ message, addressLookupTableAccounts, }) {
|
|
47
|
+
const compiledMessage = compileToWrappedMessageV0({
|
|
48
|
+
payerKey: message.payerKey,
|
|
49
|
+
instructions: message.instructions,
|
|
50
|
+
addressLookupTableAccounts,
|
|
51
|
+
});
|
|
52
|
+
const encoder = getVaultTransactionMessageEncoder();
|
|
53
|
+
let compiled_ixs = [];
|
|
54
|
+
for (const msg_cix of compiledMessage.compiledInstructions) {
|
|
55
|
+
compiled_ixs.push({
|
|
56
|
+
programIdIndex: msg_cix.programIdIndex,
|
|
57
|
+
accountIndexes: new Uint8Array(msg_cix.accountKeyIndexes ?? []),
|
|
58
|
+
data: msg_cix.data ?? new Uint8Array(),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
let alts = [];
|
|
62
|
+
for (const alt of compiledMessage.addressTableLookups) {
|
|
63
|
+
alts.push({
|
|
64
|
+
accountKey: fromLegacyPublicKey(alt.accountKey),
|
|
65
|
+
writableIndexes: Uint8Array.from(alt.writableIndexes),
|
|
66
|
+
readonlyIndexes: Uint8Array.from(alt.readonlyIndexes)
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
const bytes = encoder.encode({
|
|
70
|
+
numSigners: compiledMessage.header.numRequiredSignatures,
|
|
71
|
+
numWritableSigners: compiledMessage.header.numRequiredSignatures - compiledMessage.header.numReadonlySignedAccounts,
|
|
72
|
+
numWritableNonSigners: compiledMessage.staticAccountKeys.length - compiledMessage.header.numRequiredSignatures - compiledMessage.header.numReadonlyUnsignedAccounts,
|
|
73
|
+
accountKeys: compiledMessage.staticAccountKeys.map((pk) => fromLegacyPublicKey(pk)),
|
|
74
|
+
addressTableLookups: alts,
|
|
75
|
+
instructions: compiled_ixs,
|
|
76
|
+
});
|
|
77
|
+
return bytes;
|
|
78
|
+
}
|
|
79
|
+
export function toLegacyTransactionInstruction(instruction, signerAccountIndexes) {
|
|
80
|
+
const signerSet = new Set(signerAccountIndexes);
|
|
81
|
+
const keys = (instruction.accounts ?? []).map((acct, index) => {
|
|
82
|
+
const pubkey = new PublicKey(acct.address);
|
|
83
|
+
// base flags from role
|
|
84
|
+
let { isSigner, isWritable } = roleToFlags(acct.role);
|
|
85
|
+
// 🔥 FORCE signer if index is listed
|
|
86
|
+
if (signerSet.has(index)) {
|
|
87
|
+
isSigner = true;
|
|
88
|
+
}
|
|
89
|
+
return { pubkey, isSigner, isWritable };
|
|
90
|
+
});
|
|
91
|
+
const data = instruction.data instanceof Uint8Array
|
|
92
|
+
? Buffer.from(instruction.data)
|
|
93
|
+
: Buffer.alloc(0);
|
|
94
|
+
const programId = new PublicKey(instruction.programAddress);
|
|
95
|
+
return new TransactionInstruction({
|
|
96
|
+
keys,
|
|
97
|
+
programId,
|
|
98
|
+
data,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
// Convert AccountRole back to isSigner / isWritable flags
|
|
102
|
+
function roleToFlags(role) {
|
|
103
|
+
switch (role) {
|
|
104
|
+
case AccountRole.WRITABLE_SIGNER:
|
|
105
|
+
return { isSigner: true, isWritable: true };
|
|
106
|
+
case AccountRole.READONLY_SIGNER:
|
|
107
|
+
return { isSigner: true, isWritable: false };
|
|
108
|
+
case AccountRole.WRITABLE:
|
|
109
|
+
return { isSigner: false, isWritable: true };
|
|
110
|
+
case AccountRole.READONLY:
|
|
111
|
+
default:
|
|
112
|
+
return { isSigner: false, isWritable: false };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** Populate remaining accounts required for execution of the transaction. */
|
|
116
|
+
export async function accountsForTransactionExecute({ connection, transactionPda, vaultPda, message, ephemeralSignerBumps, programId, addressLookupTableAccounts: localAddressLookupTableAccounts, }) {
|
|
117
|
+
const ephemeralSignerPdas = await Promise.all(ephemeralSignerBumps.map(async (_, additionalSignerIndex) => {
|
|
118
|
+
const [pda] = await getEphemeralSignerPda({
|
|
119
|
+
transactionPda: transactionPda,
|
|
120
|
+
ephemeralSignerIndex: additionalSignerIndex,
|
|
121
|
+
});
|
|
122
|
+
return pda;
|
|
123
|
+
}));
|
|
124
|
+
const addressLookupTableKeys = message.addressTableLookups.map(({ accountKey }) => accountKey);
|
|
125
|
+
const addressLookupTableAccounts = new Map(await Promise.all(addressLookupTableKeys.map(async (key) => {
|
|
126
|
+
const localAccount = localAddressLookupTableAccounts?.find((a) => a.key.toBase58() === key);
|
|
127
|
+
if (localAccount) {
|
|
128
|
+
return [key, localAccount];
|
|
129
|
+
}
|
|
130
|
+
const { value } = await connection.getAddressLookupTable(new PublicKey(key));
|
|
131
|
+
if (!value) {
|
|
132
|
+
throw new Error(`Address lookup table account ${key} not found`);
|
|
133
|
+
}
|
|
134
|
+
return [key, value];
|
|
135
|
+
})));
|
|
136
|
+
// Populate account metas required for execution of the transaction.
|
|
137
|
+
const accountMetas = [];
|
|
138
|
+
// First add the lookup table accounts used by the transaction. They are needed for on-chain validation.
|
|
139
|
+
accountMetas.push(...addressLookupTableKeys.map((key) => {
|
|
140
|
+
return { pubkey: new PublicKey(key), isSigner: false, isWritable: false };
|
|
141
|
+
}));
|
|
142
|
+
// Then add static account keys included into the message.
|
|
143
|
+
for (const [accountIndex, accountKey] of message.accountKeys.entries()) {
|
|
144
|
+
let accountKey_pubKey = new PublicKey(accountKey);
|
|
145
|
+
accountMetas.push({
|
|
146
|
+
pubkey: new PublicKey(accountKey),
|
|
147
|
+
isWritable: isStaticWritableIndex(message, accountIndex),
|
|
148
|
+
// NOTE: vaultPda and ephemeralSignerPdas cannot be marked as signers,
|
|
149
|
+
// because they are PDAs and hence won't have their signatures on the transaction.
|
|
150
|
+
isSigner: isSignerIndex(message, accountIndex) &&
|
|
151
|
+
!accountKey_pubKey.equals(vaultPda) &&
|
|
152
|
+
!ephemeralSignerPdas.some((k) => accountKey_pubKey.equals(k)),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
// Then add accounts that will be loaded with address lookup tables.
|
|
156
|
+
for (const lookup of message.addressTableLookups) {
|
|
157
|
+
const lookupTableAccount = addressLookupTableAccounts.get(lookup.accountKey);
|
|
158
|
+
invariant(lookupTableAccount, `Address lookup table account ${lookup.accountKey} not found`);
|
|
159
|
+
for (const accountIndex of lookup.writableIndexes) {
|
|
160
|
+
const pubkey = lookupTableAccount.state.addresses[accountIndex];
|
|
161
|
+
invariant(pubkey, `Address lookup table account ${lookup.accountKey} does not contain address at index ${accountIndex}`);
|
|
162
|
+
accountMetas.push({
|
|
163
|
+
pubkey,
|
|
164
|
+
isWritable: true,
|
|
165
|
+
// Accounts in address lookup tables can not be signers.
|
|
166
|
+
isSigner: false,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
for (const accountIndex of lookup.readonlyIndexes) {
|
|
170
|
+
const pubkey = lookupTableAccount.state.addresses[accountIndex];
|
|
171
|
+
invariant(pubkey, `Address lookup table account ${lookup.accountKey} does not contain address at index ${accountIndex}`);
|
|
172
|
+
accountMetas.push({
|
|
173
|
+
pubkey,
|
|
174
|
+
isWritable: false,
|
|
175
|
+
// Accounts in address lookup tables can not be signers.
|
|
176
|
+
isSigner: false,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
accountMetas,
|
|
182
|
+
lookupTableAccounts: [...addressLookupTableAccounts.values()],
|
|
183
|
+
};
|
|
184
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fortis-multisig-client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Fortis Solana multisig generated client",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -29,4 +29,4 @@
|
|
|
29
29
|
"@types/node": "^20.0.0",
|
|
30
30
|
"typescript": "^5.6.2"
|
|
31
31
|
}
|
|
32
|
-
}
|
|
32
|
+
}
|