Add wasm bindings for Pubkey and Keypair

This commit is contained in:
Michael Vines
2021-10-13 16:52:52 -07:00
parent 6919c4863b
commit 488dc37fec
23 changed files with 537 additions and 15 deletions

4
sdk/.gitignore vendored
View File

@ -1,2 +1,4 @@
/target/
/farf/
/node_modules/
/package-lock.json
/target/

View File

@ -77,6 +77,10 @@ solana-program = { path = "program", version = "=1.10.0" }
solana-sdk-macro = { path = "macro", version = "=1.10.0" }
thiserror = "1.0"
uriparse = "0.6.3"
wasm-bindgen = "0.2"
[target.'cfg(target_arch = "wasm32")'.dependencies]
js-sys = "0.3.55"
[dev-dependencies]
curve25519-dalek = "3.2.0"

View File

@ -373,3 +373,31 @@ pub fn pubkeys(input: TokenStream) -> TokenStream {
let pubkeys = parse_macro_input!(input as Pubkeys);
TokenStream::from(quote! {#pubkeys})
}
// The normal `wasm_bindgen` macro generates a .bss section which causes the resulting
// BPF program to fail to load, so for now this stub should be used when building for BPF
#[proc_macro_attribute]
pub fn wasm_bindgen_stub(_attr: TokenStream, item: TokenStream) -> TokenStream {
match parse_macro_input!(item as syn::Item) {
syn::Item::Struct(mut item_struct) => {
if let syn::Fields::Named(fields) = &mut item_struct.fields {
// Strip out any `#[wasm_bindgen]` added to struct fields. This is custom
// syntax supplied by the normal `wasm_bindgen` macro.
for field in fields.named.iter_mut() {
field.attrs.retain(|attr| {
!attr
.path
.segments
.iter()
.any(|segment| segment.ident == "wasm_bindgen")
});
}
}
quote! { #item_struct }
}
item => {
quote!(#item)
}
}
.into()
}

1
sdk/package.json Symbolic link
View File

@ -0,0 +1 @@
program/package.json

2
sdk/program/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/node_modules/
/package-lock.json

View File

@ -42,6 +42,13 @@ libsecp256k1 = "0.6.0"
rand = "0.7.0"
solana-logger = { path = "../../logger", version = "=1.10.0" }
itertools = "0.10.1"
wasm-bindgen = "0.2"
[target.'cfg(target_arch = "wasm32")'.dependencies]
console_error_panic_hook = "0.1.7"
console_log = "0.2.0"
js-sys = "0.3.55"
getrandom = { version = "0.1", features = ["wasm-bindgen"] }
[target.'cfg(not(target_pointer_width = "64"))'.dependencies]
parking_lot = "0.11"

14
sdk/program/package.json Normal file
View File

@ -0,0 +1,14 @@
{
"devDependencies": {
"chai": "^4.3.4",
"mocha": "^9.1.2",
"prettier": "^2.4.1"
},
"scripts": {
"postinstall": "npm run build",
"build": "wasm-pack build --target nodejs --dev --out-dir node_modules/crate --out-name crate",
"pretty": "prettier --check 'tests/*.mjs'",
"pretty:fix": "prettier --write 'tests/*.mjs'",
"test": "mocha 'tests/*.mjs'"
}
}

View File

@ -52,6 +52,12 @@ pub mod stake_history;
pub mod system_instruction;
pub mod system_program;
pub mod sysvar;
pub mod wasm;
#[cfg(target_arch = "bpf")]
pub use solana_sdk_macro::wasm_bindgen_stub as wasm_bindgen;
#[cfg(not(target_arch = "bpf"))]
pub use wasm_bindgen::prelude::wasm_bindgen;
pub mod config {
pub mod program {

View File

@ -1,6 +1,6 @@
#![allow(clippy::integer_arithmetic)]
use {
crate::{decode_error::DecodeError, hash::hashv},
crate::{decode_error::DecodeError, hash::hashv, wasm_bindgen},
borsh::{BorshDeserialize, BorshSchema, BorshSerialize},
bytemuck::{Pod, Zeroable},
num_derive::{FromPrimitive, ToPrimitive},
@ -48,6 +48,7 @@ impl From<u64> for PubkeyError {
}
}
#[wasm_bindgen]
#[repr(transparent)]
#[derive(
AbiExample,
@ -67,7 +68,7 @@ impl From<u64> for PubkeyError {
Serialize,
Zeroable,
)]
pub struct Pubkey([u8; 32]);
pub struct Pubkey(pub(crate) [u8; 32]);
impl crate::sanitize::Sanitize for Pubkey {}

View File

@ -0,0 +1,16 @@
//! solana-program Javascript interface
#![cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;
pub mod pubkey;
/// Initialize Javascript logging and panic handler
#[wasm_bindgen]
pub fn init() {
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
console_log::init_with_level(log::Level::Info).unwrap();
}
pub fn display_to_jsvalue<T: std::fmt::Display>(display: T) -> JsValue {
display.to_string().into()
}

View File

@ -0,0 +1,121 @@
//! `Pubkey` Javascript interface
#![cfg(target_arch = "wasm32")]
#![allow(non_snake_case)]
use {
crate::{pubkey::*, wasm::display_to_jsvalue},
js_sys::{Array, Uint8Array},
wasm_bindgen::{prelude::*, JsCast},
};
fn js_value_to_seeds_vec(array_of_uint8_arrays: &[JsValue]) -> Result<Vec<Vec<u8>>, JsValue> {
let vec_vec_u8 = array_of_uint8_arrays
.iter()
.filter_map(|u8_array| {
u8_array
.dyn_ref::<Uint8Array>()
.map(|u8_array| u8_array.to_vec())
})
.collect::<Vec<_>>();
if vec_vec_u8.len() != array_of_uint8_arrays.len() {
Err("Invalid Array of Uint8Arrays".into())
} else {
Ok(vec_vec_u8)
}
}
#[wasm_bindgen]
impl Pubkey {
/// Create a new Pubkey object
///
/// * `value` - optional public key as a base58 encoded string, `Uint8Array`, `[number]`
#[wasm_bindgen(constructor)]
pub fn constructor(value: JsValue) -> Result<Pubkey, JsValue> {
if let Some(base58_str) = value.as_string() {
base58_str.parse::<Pubkey>().map_err(display_to_jsvalue)
} else if let Some(uint8_array) = value.dyn_ref::<Uint8Array>() {
Ok(Pubkey::new(&uint8_array.to_vec()))
} else if let Some(array) = value.dyn_ref::<Array>() {
let mut bytes = vec![];
let iterator = js_sys::try_iter(&array.values())?.expect("array to be iterable");
for x in iterator {
let x = x?;
if let Some(n) = x.as_f64() {
if n >= 0. && n <= 255. {
bytes.push(n as u8);
continue;
}
}
return Err(format!("Invalid array argument: {:?}", x).into());
}
Ok(Pubkey::new(&bytes))
} else if value.is_undefined() {
Ok(Pubkey::default())
} else {
Err("Unsupported argument".into())
}
}
/// Return the base58 string representation of the public key
pub fn toString(&self) -> String {
self.to_string()
}
/// Check if a `Pubkey` is on the ed25519 curve.
pub fn isOnCurve(&self) -> bool {
self.is_on_curve()
}
/// Checks if two `Pubkey`s are equal
pub fn equals(&self, other: &Pubkey) -> bool {
self == other
}
/// Return the `Uint8Array` representation of the public key
pub fn toBytes(&self) -> Box<[u8]> {
self.0.clone().into()
}
/// Derive a Pubkey from another Pubkey, string seed, and a program id
pub fn createWithSeed(base: &Pubkey, seed: &str, owner: &Pubkey) -> Result<Pubkey, JsValue> {
Pubkey::create_with_seed(base, seed, owner).map_err(display_to_jsvalue)
}
/// Derive a program address from seeds and a program id
pub fn createProgramAddress(
seeds: Box<[JsValue]>,
program_id: &Pubkey,
) -> Result<Pubkey, JsValue> {
let seeds_vec = js_value_to_seeds_vec(&seeds)?;
let seeds_slice = seeds_vec
.iter()
.map(|seed| seed.as_slice())
.collect::<Vec<_>>();
Pubkey::create_program_address(seeds_slice.as_slice(), program_id)
.map_err(display_to_jsvalue)
}
/// Find a valid program address
///
/// Returns:
/// * `[PubKey, number]` - the program address and bump seed
pub fn findProgramAddress(
seeds: Box<[JsValue]>,
program_id: &Pubkey,
) -> Result<JsValue, JsValue> {
let seeds_vec = js_value_to_seeds_vec(&seeds)?;
let seeds_slice = seeds_vec
.iter()
.map(|seed| seed.as_slice())
.collect::<Vec<_>>();
let (address, bump_seed) = Pubkey::find_program_address(seeds_slice.as_slice(), program_id);
let result = Array::new_with_length(2);
result.set(0, address.into());
result.set(1, bump_seed.into());
Ok(result.into())
}
}

View File

@ -0,0 +1,185 @@
import { expect } from "chai";
import { init, Pubkey } from "crate";
init();
// TODO: wasm_bindgen doesn't currently support exporting constants
const MAX_SEED_LEN = 32;
describe("Pubkey", function () {
it("invalid", () => {
expect(() => {
new Pubkey([
3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
]);
}).to.throw();
expect(() => {
new Pubkey([
'invalid', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0,
]);
}).to.throw();
expect(() => {
new Pubkey(
"0x300000000000000000000000000000000000000000000000000000000000000000000"
);
}).to.throw();
expect(() => {
new Pubkey(
"0x300000000000000000000000000000000000000000000000000000000000000"
);
}).to.throw();
expect(() => {
new Pubkey(
"135693854574979916511997248057056142015550763280047535983739356259273198796800000"
);
}).to.throw();
expect(() => {
new Pubkey("12345");
}).to.throw();
});
it("toString", () => {
const key = new Pubkey("CiDwVBFgWV9E5MvXWoLgnEgn2hK7rJikbvfWavzAQz3");
expect(key.toString()).to.eq("CiDwVBFgWV9E5MvXWoLgnEgn2hK7rJikbvfWavzAQz3");
const key2 = new Pubkey("1111111111111111111111111111BukQL");
expect(key2.toString()).to.eq("1111111111111111111111111111BukQL");
const key3 = new Pubkey("11111111111111111111111111111111");
expect(key3.toString()).to.eq("11111111111111111111111111111111");
const key4 = new Pubkey([
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0,
]);
expect(key4.toString()).to.eq("11111111111111111111111111111111");
});
it("toBytes", () => {
const key = new Pubkey("CiDwVBFgWV9E5MvXWoLgnEgn2hK7rJikbvfWavzAQz3");
expect(key.toBytes()).to.deep.equal(
new Uint8Array([
3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
])
);
const key2 = new Pubkey();
expect(key2.toBytes()).to.deep.equal(
new Uint8Array([
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
])
);
});
it("isOnCurve", () => {
let onCurve = new Pubkey("J4NYrSRccTUGXP7wmFwiByakqWKZb5RwpiAoskpgAQRb");
expect(onCurve.isOnCurve()).to.be.true;
let offCurve = new Pubkey("12rqwuEgBYiGhBrDJStCiqEtzQpTTiZbh7teNVLuYcFA");
expect(offCurve.isOnCurve()).to.be.false;
});
it("equals", () => {
const arrayKey = new Pubkey([
3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0,
]);
const base58Key = new Pubkey("CiDwVBFgWV9E5MvXWoLgnEgn2hK7rJikbvfWavzAQz3");
expect(arrayKey.equals(base58Key)).to.be.true;
});
it("createWithSeed", async () => {
const defaultPublicKey = new Pubkey("11111111111111111111111111111111");
const derivedKey = Pubkey.createWithSeed(
defaultPublicKey,
"limber chicken: 4/45",
defaultPublicKey
);
expect(
derivedKey.equals(
new Pubkey("9h1HyLCW5dZnBVap8C5egQ9Z6pHyjsh5MNy83iPqqRuq")
)
).to.be.true;
});
it("createProgramAddress", async () => {
const programId = new Pubkey("BPFLoader1111111111111111111111111111111111");
const publicKey = new Pubkey("SeedPubey1111111111111111111111111111111111");
let programAddress = Pubkey.createProgramAddress(
[Buffer.from("", "utf8"), Buffer.from([1])],
programId
);
expect(
programAddress.equals(
new Pubkey("3gF2KMe9KiC6FNVBmfg9i267aMPvK37FewCip4eGBFcT")
)
).to.be.true;
programAddress = Pubkey.createProgramAddress(
[Buffer.from("☉", "utf8")],
programId
);
expect(
programAddress.equals(
new Pubkey("7ytmC1nT1xY4RfxCV2ZgyA7UakC93do5ZdyhdF3EtPj7")
)
).to.be.true;
programAddress = Pubkey.createProgramAddress(
[Buffer.from("Talking", "utf8"), Buffer.from("Squirrels", "utf8")],
programId
);
expect(
programAddress.equals(
new Pubkey("HwRVBufQ4haG5XSgpspwKtNd3PC9GM9m1196uJW36vds")
)
).to.be.true;
programAddress = Pubkey.createProgramAddress(
[publicKey.toBytes()],
programId
);
expect(
programAddress.equals(
new Pubkey("GUs5qLUfsEHkcMB9T38vjr18ypEhRuNWiePW2LoK4E3K")
)
).to.be.true;
const programAddress2 = Pubkey.createProgramAddress(
[Buffer.from("Talking", "utf8")],
programId
);
expect(programAddress.equals(programAddress2)).to.eq(false);
expect(() => {
Pubkey.createProgramAddress([Buffer.alloc(MAX_SEED_LEN + 1)], programId);
}).to.throw();
});
it("findProgramAddress", async () => {
const programId = new Pubkey("BPFLoader1111111111111111111111111111111111");
let [programAddress, nonce] = Pubkey.findProgramAddress(
[Buffer.from("", "utf8")],
programId
);
expect(
programAddress.equals(
Pubkey.createProgramAddress(
[Buffer.from("", "utf8"), Buffer.from([nonce])],
programId
)
)
).to.be.true;
});
});

View File

@ -47,6 +47,7 @@ pub mod system_transaction;
pub mod timing;
pub mod transaction;
pub mod transport;
pub mod wasm;
/// Same as `declare_id` except report that this id has been deprecated
pub use solana_sdk_macro::declare_deprecated_id;

View File

@ -17,9 +17,11 @@ use {
io::{Read, Write},
path::Path,
},
wasm_bindgen::prelude::*,
};
/// A vanilla Ed25519 key pair
#[wasm_bindgen]
#[derive(Debug)]
pub struct Keypair(ed25519_dalek::Keypair);

34
sdk/src/wasm/keypair.rs Normal file
View File

@ -0,0 +1,34 @@
//! `Keypair` Javascript interface
#![cfg(target_arch = "wasm32")]
#![allow(non_snake_case)]
use {
crate::signer::{keypair::Keypair, Signer},
solana_program::{pubkey::Pubkey, wasm::display_to_jsvalue},
wasm_bindgen::prelude::*,
};
#[wasm_bindgen]
impl Keypair {
/// Create a new `Keypair `
#[wasm_bindgen(constructor)]
pub fn constructor() -> Keypair {
Keypair::new()
}
/// Convert a `Keypair` to a `Uint8Array`
pub fn toBytes(&self) -> Box<[u8]> {
self.to_bytes().into()
}
/// Recover a `Keypair` from a `Uint8Array`
pub fn fromBytes(bytes: &[u8]) -> Result<Keypair, JsValue> {
Keypair::from_bytes(bytes).map_err(display_to_jsvalue)
}
/// Return the `Pubkey` for this `Keypair`
#[wasm_bindgen(js_name = pubkey)]
pub fn js_pubkey(&self) -> Pubkey {
// `wasm_bindgen` does not support traits (`Signer) yet
self.pubkey()
}
}

4
sdk/src/wasm/mod.rs Normal file
View File

@ -0,0 +1,4 @@
//! solana-sdk Javascript interface
#![cfg(target_arch = "wasm32")]
pub mod keypair;

14
sdk/tests/keypair.mjs Normal file
View File

@ -0,0 +1,14 @@
import { expect } from "chai";
import { init, Keypair } from "crate";
init();
describe("Keypair", function () {
it("works", () => {
const keypair = new Keypair();
let bytes = keypair.toBytes();
expect(bytes).to.have.length(64);
const recoveredKeypair = Keypair.fromBytes(bytes);
expect(keypair.pubkey().equals(recoveredKeypair.pubkey()));
});
});