index.ts 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. import {
  2. Keypair,
  3. Connection,
  4. BpfLoader,
  5. BPF_LOADER_PROGRAM_ID,
  6. PublicKey,
  7. LAMPORTS_PER_SOL,
  8. SystemProgram,
  9. TransactionInstruction,
  10. Transaction,
  11. sendAndConfirmTransaction, SYSVAR_CLOCK_PUBKEY,
  12. } from '@solana/web3.js';
  13. import fs from 'fs';
  14. import { AbiItem } from 'web3-utils';
  15. import { utils } from 'ethers';
  16. import crypto from 'crypto';
  17. import { SigningKey } from 'ethers/lib/utils';
  18. const Web3EthAbi = require('web3-eth-abi');
  19. const default_url: string = "http://localhost:8899";
  20. export async function establishConnection(): Promise<TestConnection> {
  21. let url = process.env.RPC_URL || default_url;
  22. let connection = new Connection(url, 'recent');
  23. const version = await connection.getVersion();
  24. console.log('Connection to cluster established:', url, version);
  25. // Fund a new payer via airdrop
  26. let payerAccount = await newAccountWithLamports(connection);
  27. const lamports = await connection.getBalance(payerAccount.publicKey);
  28. console.log(
  29. 'Using account',
  30. payerAccount.publicKey.toBase58(),
  31. 'containing',
  32. lamports / LAMPORTS_PER_SOL,
  33. 'Sol to pay for fees',
  34. );
  35. return new TestConnection(connection, payerAccount);
  36. }
  37. export async function createProgramAddress(program: PublicKey): Promise<any> {
  38. while (true) {
  39. let seed = crypto.randomBytes(7);
  40. let pda: any = undefined;
  41. await PublicKey.createProgramAddress([seed], program).then(v => { pda = v; }).catch(_ => { });
  42. if (pda) {
  43. return { address: pda, seed };
  44. }
  45. }
  46. }
  47. const sleep = (ms: number) => new Promise(res => setTimeout(res, ms));
  48. async function newAccountWithLamports(
  49. connection: Connection,
  50. lamports: number = 10000000000,
  51. ): Promise<Keypair> {
  52. const account = Keypair.generate();
  53. let retries = 10;
  54. await connection.requestAirdrop(account.publicKey, lamports);
  55. for (; ;) {
  56. await sleep(500);
  57. if (lamports == (await connection.getBalance(account.publicKey))) {
  58. return account;
  59. }
  60. if (--retries <= 0) {
  61. break;
  62. }
  63. console.log('Airdrop retry ' + retries);
  64. }
  65. throw new Error(`Airdrop of ${lamports} failed`);
  66. }
  67. class TestConnection {
  68. constructor(public connection: Connection, public payerAccount: Keypair) { }
  69. async createStorageAccount(programId: PublicKey, space: number): Promise<Keypair> {
  70. const lamports = await this.connection.getMinimumBalanceForRentExemption(
  71. space
  72. );
  73. let account = Keypair.generate();
  74. const transaction = new Transaction().add(
  75. SystemProgram.createAccount({
  76. fromPubkey: this.payerAccount.publicKey,
  77. newAccountPubkey: account.publicKey,
  78. lamports,
  79. space,
  80. programId,
  81. }),
  82. );
  83. await sendAndConfirmTransaction(
  84. this.connection,
  85. transaction,
  86. [this.payerAccount, account],
  87. {
  88. skipPreflight: false,
  89. commitment: 'recent',
  90. preflightCommitment: undefined,
  91. },
  92. );
  93. console.log('Contract storage account', account.publicKey.toBase58());
  94. return account;
  95. }
  96. async loadProgram(sopath: string, abipath: string, contractStorageSize: number = 2048): Promise<Program> {
  97. console.log(`Loading ${sopath} ...`)
  98. const data: Buffer = fs.readFileSync(sopath);
  99. const abi: string = fs.readFileSync(abipath, 'utf-8');
  100. const programAccount = Keypair.generate();
  101. await BpfLoader.load(
  102. this.connection,
  103. this.payerAccount,
  104. programAccount,
  105. data,
  106. BPF_LOADER_PROGRAM_ID,
  107. );
  108. const programId = programAccount.publicKey;
  109. console.log('Program loaded to account', programId.toBase58());
  110. const contractStorageAccount = await this.createStorageAccount(programId, contractStorageSize);
  111. return new Program(programId, contractStorageAccount, abi);
  112. }
  113. }
  114. class Program {
  115. constructor(private programId: PublicKey, private contractStorageAccount: Keypair, private abi: string) { }
  116. encode_seeds(seeds: any[]): Buffer {
  117. let seed_encoded = Buffer.alloc(1 + seeds.map(seed => seed.seed.length + 1).reduce((a, b) => a + b, 0));
  118. seed_encoded.writeUInt8(seeds.length);
  119. let offset = 1;
  120. seeds.forEach((v) => {
  121. let seed = v.seed;
  122. seed_encoded.writeUInt8(seed.length, offset);
  123. offset += 1;
  124. seed.copy(seed_encoded, offset);
  125. offset += seed.length;
  126. });
  127. return seed_encoded;
  128. }
  129. async call_constructor(test: TestConnection, contract: string, params: string[], seeds: any[] = []): Promise<void> {
  130. let abi: AbiItem | undefined = JSON.parse(this.abi).find((e: AbiItem) => e.type == "constructor");
  131. let inputs = abi?.inputs! || [];
  132. const input = Web3EthAbi.encodeParameters(inputs, params);
  133. let hash = utils.keccak256(Buffer.from(contract));
  134. const data = Buffer.concat([
  135. this.contractStorageAccount.publicKey.toBuffer(),
  136. test.payerAccount.publicKey.toBuffer(),
  137. Buffer.from(hash.substr(2, 8), 'hex'),
  138. this.encode_seeds(seeds),
  139. Buffer.from(input.replace('0x', ''), 'hex')
  140. ]);
  141. console.log('calling constructor [' + params + ']');
  142. const instruction = new TransactionInstruction({
  143. keys: [
  144. { pubkey: this.contractStorageAccount.publicKey, isSigner: false, isWritable: true }],
  145. programId: this.programId,
  146. data,
  147. });
  148. await sendAndConfirmTransaction(
  149. test.connection,
  150. new Transaction().add(instruction),
  151. [test.payerAccount],
  152. {
  153. skipPreflight: false,
  154. commitment: 'recent',
  155. preflightCommitment: undefined,
  156. },
  157. );
  158. }
  159. async call_function(test: TestConnection, name: string, params: any[], pubkeys: PublicKey[] = [], seeds: any[] = [], signers: Keypair[] = []): Promise<{ [key: string]: any }> {
  160. let abi: AbiItem = JSON.parse(this.abi).find((e: AbiItem) => e.name == name);
  161. const input: string = Web3EthAbi.encodeFunctionCall(abi, params);
  162. const data = Buffer.concat([
  163. this.contractStorageAccount.publicKey.toBuffer(),
  164. test.payerAccount.publicKey.toBuffer(),
  165. Buffer.from('00000000', 'hex'),
  166. this.encode_seeds(seeds),
  167. Buffer.from(input.replace('0x', ''), 'hex')
  168. ]);
  169. let debug = 'calling function ' + name + ' [' + params + ']';
  170. let keys = [];
  171. seeds.forEach((seed) => {
  172. keys.push({ pubkey: seed.address, isSigner: false, isWritable: true });
  173. });
  174. keys.push({ pubkey: this.contractStorageAccount.publicKey, isSigner: false, isWritable: true });
  175. keys.push({ pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false });
  176. keys.push({ pubkey: PublicKey.default, isSigner: false, isWritable: false });
  177. for (let i = 0; i < pubkeys.length; i++) {
  178. keys.push({ pubkey: pubkeys[i], isSigner: false, isWritable: true });
  179. }
  180. const instruction = new TransactionInstruction({
  181. keys,
  182. programId: this.programId,
  183. data,
  184. });
  185. signers.unshift(test.payerAccount);
  186. await sendAndConfirmTransaction(
  187. test.connection,
  188. new Transaction().add(instruction),
  189. signers,
  190. {
  191. skipPreflight: false,
  192. commitment: 'recent',
  193. preflightCommitment: undefined,
  194. },
  195. );
  196. if (abi.outputs?.length) {
  197. const accountInfo = await test.connection.getAccountInfo(this.contractStorageAccount.publicKey);
  198. let length = Number(accountInfo!.data.readUInt32LE(4));
  199. let offset = Number(accountInfo!.data.readUInt32LE(8));
  200. let encoded = accountInfo!.data.slice(offset, length + offset);
  201. let returns = Web3EthAbi.decodeParameters(abi.outputs!, encoded.toString('hex'));
  202. debug += " returns [";
  203. for (let i = 0; i.toString() in returns; i++) {
  204. debug += returns[i];
  205. }
  206. debug += "]"
  207. console.log(debug);
  208. return returns;
  209. } else {
  210. console.log(debug);
  211. return [];
  212. }
  213. }
  214. async contract_storage(test: TestConnection, upto: number): Promise<Buffer> {
  215. const accountInfo = await test.connection.getAccountInfo(this.contractStorageAccount.publicKey);
  216. return accountInfo!.data;
  217. }
  218. all_keys(): PublicKey[] {
  219. return [this.programId, this.contractStorageAccount.publicKey];
  220. }
  221. get_program_key(): PublicKey {
  222. return this.programId;
  223. }
  224. get_storage_keypair(): Keypair {
  225. return this.contractStorageAccount;
  226. }
  227. }