diff --git a/sdk/program/src/instruction.rs b/sdk/program/src/instruction.rs index 51324ab60..803e95596 100644 --- a/sdk/program/src/instruction.rs +++ b/sdk/program/src/instruction.rs @@ -2,7 +2,7 @@ //! Defines a composable Instruction type and a memory-efficient CompiledInstruction. use { - crate::{pubkey::Pubkey, sanitize::Sanitize, short_vec}, + crate::{pubkey::Pubkey, sanitize::Sanitize, short_vec, wasm_bindgen}, bincode::serialize, borsh::BorshSerialize, serde::Serialize, @@ -240,13 +240,17 @@ pub enum InstructionError { // conversions must also be added } +#[wasm_bindgen] #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] pub struct Instruction { /// Pubkey of the instruction processor that executes this instruction + #[wasm_bindgen(skip)] pub program_id: Pubkey, /// Metadata for what accounts should be passed to the instruction processor + #[wasm_bindgen(skip)] pub accounts: Vec, /// Opaque data passed to the instruction processor + #[wasm_bindgen(skip)] pub data: Vec, } diff --git a/sdk/program/src/message/legacy.rs b/sdk/program/src/message/legacy.rs index 986b94b31..a02ff8d6a 100644 --- a/sdk/program/src/message/legacy.rs +++ b/sdk/program/src/message/legacy.rs @@ -12,7 +12,7 @@ use { serialize_utils::{ append_slice, append_u16, append_u8, read_pubkey, read_slice, read_u16, read_u8, }, - short_vec, system_instruction, system_program, sysvar, + short_vec, system_instruction, system_program, sysvar, wasm_bindgen, }, lazy_static::lazy_static, std::{collections::BTreeSet, convert::TryFrom, str::FromStr}, @@ -168,15 +168,18 @@ fn get_program_ids(instructions: &[Instruction]) -> Vec { // NOTE: Serialization-related changes must be paired with the custom serialization // for versioned messages in the `RemainingLegacyMessage` struct. +#[wasm_bindgen] #[frozen_abi(digest = "2KnLEqfLcTBQqitE22Pp8JYkaqVVbAkGbCfdeHoyxcAU")] #[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq, Clone, AbiExample)] #[serde(rename_all = "camelCase")] pub struct Message { /// The message header, identifying signed and read-only `account_keys` /// NOTE: Serialization-related changes must be paired with the direct read at sigverify. + #[wasm_bindgen(skip)] pub header: MessageHeader, /// All the account keys used by this transaction + #[wasm_bindgen(skip)] #[serde(with = "short_vec")] pub account_keys: Vec, @@ -185,6 +188,7 @@ pub struct Message { /// Programs that will be executed in sequence and committed in one atomic transaction if all /// succeed. + #[wasm_bindgen(skip)] #[serde(with = "short_vec")] pub instructions: Vec, } diff --git a/sdk/program/src/system_instruction.rs b/sdk/program/src/system_instruction.rs index 59c625514..1ae02021d 100644 --- a/sdk/program/src/system_instruction.rs +++ b/sdk/program/src/system_instruction.rs @@ -1,5 +1,4 @@ #[allow(deprecated)] -use crate::sysvar::recent_blockhashes; use { crate::{ decode_error::DecodeError, @@ -7,7 +6,7 @@ use { nonce, pubkey::Pubkey, system_program, - sysvar::rent, + sysvar::{recent_blockhashes, rent}, }, num_derive::{FromPrimitive, ToPrimitive}, thiserror::Error, diff --git a/sdk/program/src/wasm/instructions.rs b/sdk/program/src/wasm/instructions.rs new file mode 100644 index 000000000..36abe05c6 --- /dev/null +++ b/sdk/program/src/wasm/instructions.rs @@ -0,0 +1,28 @@ +//! The `Instructions` struct is a workaround for the lack of Vec support in wasm-bindgen +//! (ref: https://github.com/rustwasm/wasm-bindgen/issues/111) +#![cfg(target_arch = "wasm32")] +use {crate::instruction::Instruction, wasm_bindgen::prelude::*}; + +#[wasm_bindgen] +#[derive(Default)] +pub struct Instructions { + instructions: Vec, +} + +#[wasm_bindgen] +impl Instructions { + #[wasm_bindgen(constructor)] + pub fn constructor() -> Instructions { + Instructions::default() + } + + pub fn push(&mut self, instruction: Instruction) { + self.instructions.push(instruction); + } +} + +impl From for Vec { + fn from(instructions: Instructions) -> Self { + instructions.instructions + } +} diff --git a/sdk/program/src/wasm/mod.rs b/sdk/program/src/wasm/mod.rs index 63d06f9ce..801142b48 100644 --- a/sdk/program/src/wasm/mod.rs +++ b/sdk/program/src/wasm/mod.rs @@ -3,7 +3,9 @@ use wasm_bindgen::prelude::*; pub mod hash; +pub mod instructions; pub mod pubkey; +pub mod system_instruction; /// Initialize Javascript logging and panic handler #[wasm_bindgen] diff --git a/sdk/program/src/wasm/system_instruction.rs b/sdk/program/src/wasm/system_instruction.rs new file mode 100644 index 000000000..94dd63678 --- /dev/null +++ b/sdk/program/src/wasm/system_instruction.rs @@ -0,0 +1,112 @@ +//! `SystemInstruction` Javascript interface +#![cfg(target_arch = "wasm32")] +#![allow(non_snake_case)] +use { + crate::{instruction::Instruction, pubkey::Pubkey, system_instruction::*}, + wasm_bindgen::prelude::*, +}; + +#[wasm_bindgen] +impl SystemInstruction { + pub fn createAccount( + from_pubkey: &Pubkey, + to_pubkey: &Pubkey, + lamports: u64, + space: u64, + owner: &Pubkey, + ) -> Instruction { + create_account(from_pubkey, to_pubkey, lamports, space, owner) + } + + pub fn createAccountWithSeed( + from_pubkey: &Pubkey, + to_pubkey: &Pubkey, + base: &Pubkey, + seed: &str, + lamports: u64, + space: u64, + owner: &Pubkey, + ) -> Instruction { + create_account_with_seed(from_pubkey, to_pubkey, base, seed, lamports, space, owner) + } + + pub fn assign(pubkey: &Pubkey, owner: &Pubkey) -> Instruction { + assign(pubkey, owner) + } + + pub fn assignWithSeed( + pubkey: &Pubkey, + base: &Pubkey, + seed: &str, + owner: &Pubkey, + ) -> Instruction { + assign_with_seed(pubkey, base, seed, owner) + } + + pub fn transfer(from_pubkey: &Pubkey, to_pubkey: &Pubkey, lamports: u64) -> Instruction { + transfer(from_pubkey, to_pubkey, lamports) + } + + pub fn transferWithSeed( + from_pubkey: &Pubkey, + from_base: &Pubkey, + from_seed: String, + from_owner: &Pubkey, + to_pubkey: &Pubkey, + lamports: u64, + ) -> Instruction { + transfer_with_seed( + from_pubkey, + from_base, + from_seed, + from_owner, + to_pubkey, + lamports, + ) + } + + pub fn allocate(pubkey: &Pubkey, space: u64) -> Instruction { + allocate(pubkey, space) + } + + pub fn allocateWithSeed( + address: &Pubkey, + base: &Pubkey, + seed: &str, + space: u64, + owner: &Pubkey, + ) -> Instruction { + allocate_with_seed(address, base, seed, space, owner) + } + + pub fn createNonceAccount( + from_pubkey: &Pubkey, + nonce_pubkey: &Pubkey, + authority: &Pubkey, + lamports: u64, + ) -> js_sys::Array { + let instructions = create_nonce_account(from_pubkey, nonce_pubkey, authority, lamports); + instructions.into_iter().map(JsValue::from).collect() + } + + pub fn advanceNonceAccount(nonce_pubkey: &Pubkey, authorized_pubkey: &Pubkey) -> Instruction { + advance_nonce_account(nonce_pubkey, authorized_pubkey) + } + + pub fn withdrawNonceAccount( + nonce_pubkey: &Pubkey, + authorized_pubkey: &Pubkey, + to_pubkey: &Pubkey, + lamports: u64, + ) -> Instruction { + withdraw_nonce_account(nonce_pubkey, authorized_pubkey, to_pubkey, lamports) + } + + pub fn authorizeNonceAccount( + nonce_pubkey: &Pubkey, + authorized_pubkey: &Pubkey, + new_authority: &Pubkey, + ) -> Instruction { + authorize_nonce_account(nonce_pubkey, authorized_pubkey, new_authority) + } +} diff --git a/sdk/src/transaction/mod.rs b/sdk/src/transaction/mod.rs index 300ebb1fc..e81bbfef7 100644 --- a/sdk/src/transaction/mod.rs +++ b/sdk/src/transaction/mod.rs @@ -15,6 +15,7 @@ use { short_vec, signature::{Signature, SignerError}, signers::Signers, + wasm_bindgen, }, serde::Serialize, solana_program::{system_instruction::SystemInstruction, system_program}, @@ -38,6 +39,7 @@ pub enum TransactionVerificationMode { pub type Result = result::Result; /// An atomic transaction +#[wasm_bindgen] #[frozen_abi(digest = "FZtncnS1Xk8ghHfKiXE5oGiUbw2wJhmfXQuNgQR3K6Mc")] #[derive(Debug, PartialEq, Default, Eq, Clone, Serialize, Deserialize, AbiExample)] pub struct Transaction { @@ -47,10 +49,12 @@ pub struct Transaction { /// [`account_keys`]: Message::account_keys /// // NOTE: Serialization-related changes must be paired with the direct read at sigverify. + #[wasm_bindgen(skip)] #[serde(with = "short_vec")] pub signatures: Vec, /// The message to sign. + #[wasm_bindgen(skip)] pub message: Message, } diff --git a/sdk/src/wasm/mod.rs b/sdk/src/wasm/mod.rs index 021f9508f..6946e730f 100644 --- a/sdk/src/wasm/mod.rs +++ b/sdk/src/wasm/mod.rs @@ -2,3 +2,4 @@ #![cfg(target_arch = "wasm32")] pub mod keypair; +pub mod transaction; diff --git a/sdk/src/wasm/transaction.rs b/sdk/src/wasm/transaction.rs new file mode 100644 index 000000000..4b8bc6f82 --- /dev/null +++ b/sdk/src/wasm/transaction.rs @@ -0,0 +1,58 @@ +//! `Transaction` Javascript interface +#![cfg(target_arch = "wasm32")] +#![allow(non_snake_case)] +use { + crate::{ + hash::Hash, + signer::keypair::Keypair, + {message::Message, transaction::Transaction}, + }, + solana_program::{ + pubkey::Pubkey, + wasm::{display_to_jsvalue, instructions::Instructions}, + }, + wasm_bindgen::prelude::*, +}; + +#[wasm_bindgen] +impl Transaction { + /// Create a new `Transaction` + #[wasm_bindgen(constructor)] + pub fn constructor(instructions: Instructions, payer: Option) -> Transaction { + let instructions: Vec<_> = instructions.into(); + Transaction::new_with_payer(&instructions, payer.as_ref()) + } + + /// Return a message containing all data that should be signed. + #[wasm_bindgen(js_name = message)] + pub fn js_message(&self) -> Message { + self.message.clone() + } + + /// Return the serialized message data to sign. + pub fn messageData(&self) -> Box<[u8]> { + self.message_data().into() + } + + /// Verify the transaction + #[wasm_bindgen(js_name = verify)] + pub fn js_verify(&self) -> Result<(), JsValue> { + self.verify().map_err(display_to_jsvalue) + } + + pub fn partialSign(&mut self, keypair: &Keypair, recent_blockhash: &Hash) { + self.partial_sign(&[keypair], *recent_blockhash); + } + + pub fn isSigned(&self) -> bool { + self.is_signed() + } + + pub fn toBytes(&self) -> Box<[u8]> { + bincode::serialize(self).unwrap().into() + } + + pub fn fromBytes(bytes: &[u8]) -> Result { + bincode::deserialize(bytes).map_err(display_to_jsvalue) + } +} diff --git a/sdk/tests/transaction.mjs b/sdk/tests/transaction.mjs new file mode 100644 index 000000000..c672b8c46 --- /dev/null +++ b/sdk/tests/transaction.mjs @@ -0,0 +1,56 @@ +import { expect } from "chai"; +import { + init, + Pubkey, + Keypair, + Hash, + SystemInstruction, + Instructions, + Transaction, +} from "crate"; +init(); + +describe("Transaction", function () { + it("SystemInstruction::Transfer", () => { + const payer = Keypair.fromBytes( + new Uint8Array([ + 241, 230, 222, 64, 184, 48, 232, 92, 156, 210, 229, 183, 154, 251, 5, + 227, 98, 184, 34, 234, 39, 106, 62, 210, 166, 187, 31, 44, 40, 96, 24, + 51, 252, 28, 2, 120, 234, 212, 139, 111, 96, 8, 168, 204, 34, 72, 199, + 205, 117, 165, 82, 51, 32, 93, 211, 36, 239, 245, 139, 218, 99, 211, + 207, 177, + ]) + ); + + const src = Keypair.fromBytes( + new Uint8Array([ + 172, 219, 139, 103, 154, 105, 92, 23, 227, 108, 174, 80, 215, 227, 62, + 8, 66, 38, 151, 239, 148, 184, 180, 148, 149, 18, 106, 94, 73, 143, 27, + 132, 193, 64, 199, 93, 222, 83, 172, 224, 116, 205, 54, 38, 191, 178, + 149, 71, 65, 132, 46, 71, 126, 81, 63, 254, 21, 101, 90, 52, 67, 204, + 128, 199, + ]) + ); + + const dst = new Pubkey("11111111111111111111111111111112"); + + const recent_blockhash = new Hash( + "EETubP5AKHgjPAhzPAFcb8BAY1hMH639CWCFTqi3hq1k" + ); + + let instructions = new Instructions(); + instructions.push( + SystemInstruction.transfer(src.pubkey(), dst, BigInt(123)) + ); + + let transaction = new Transaction(instructions, payer.pubkey()); + transaction.partialSign(payer, recent_blockhash); + transaction.partialSign(src, recent_blockhash); + expect(transaction.isSigned()).to.be.true; + transaction.verify(); + + expect(Buffer.from(transaction.toBytes()).toString("base64")).to.equal( + "AoZrVzP93eyp3vbl6CU9XQjQfm4Xp/7nSiBlsX/kJmfTQZsGTOrFnt6EUqHVte97fGZ71UAXDfLbR5B31OtRdgdab57BOU8mq0ztMutZAVBPtGJHVly8RPz4TYa+OFU7EIk3Wrv4WUMCb/NR+LxELLH+tQt5SrkvB7rCE2DniM8JAgABBPwcAnjq1ItvYAiozCJIx811pVIzIF3TJO/1i9pj08+xwUDHXd5TrOB0zTYmv7KVR0GELkd+UT/+FWVaNEPMgMcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAxJrndgN4IFTxep3s6kO0ROug7bEsbx0xxuDkqEvwUusBAwIBAgwCAAAAewAAAAAAAAA=" + ); + }); +});