npm install --save @solar-republic/neutrinonpm install --save @cosmjs/crypto @cosmjs/amino @cosmjs/encodingnpm install --save @cosmjs/stargate
# or
npm install --save @cosmjs/cosmwasm-stargateimport { Slip10Curve, Random, Bip39, Slip10, stringToPath, Secp256k1 } from "@cosmjs/crypto"
const seed = await Bip39.mnemonicToSeed(Bip39.encode(Random.getBytes(16)));
const { privateKey } = Slip10.derivePath(Slip10Curve.Secp256k1, seed, stringToPath("m/44'/1'/0'/0"));
const pair = await Secp256k1.makeKeypair(privateKey);
// must be compressed to 33 bytes from 65
const publicKey = Secp256k1.compressPubkey(pair.pubkey);import { gen_sk, sk_to_pk } from "@solar-republic/neutrino"
const privateKey = gen_sk();
const publicKey = sk_to_pk(privateKey);import { Wallet } from "secretjs";
const { privateKey, publicKey } = new Wallet()import { SecretContract } from "@solar-republic/neutrino"
// create a contract instantse
const contract = await SecretContract(
secretNodeEndpoint,
secretContractAddress
)
// query example:
// get encryption key from a gateway contract
const queryRes = await contract.query({ encryption_key: {} })
// extract res value
const gatewayPublicKey = queryRes[2]import { SecretNetworkClient } from "secretjs"
// create a client:
const client = new SecretNetworkClient({
chainId,
url // endpoint URL of the node
});
// query the contact and get the value directly
const gatewayPublicKey = await client.query.compute.queryContract({
contract_address
code_hash, // optionally
{ encryption_key: {} } // query msg
});// window.keplr.signArbi....
signArbitrary(chainId: string, signer: string, data: string | Uint8Array) : Promise<StdSignature>// In browser environment we can get it from a wallet extension. E.g with Keplr:
const signer = window.keplr.getOfflineSigner(chainId);
// ...
// In Node environment we can use `Secp256k1Wallet` class that also implements `OfflineAminoSigner` interface
import { Secp256k1Wallet } from "@cosmjs/amino"
// see examples of generating a random above but in this case you will probably be using a persistent one from a .env file
// you can also pass extra options like prefix as the second argument
const signer = await Secp256k1HdWallet.fromMnemonic(userMnemonic)
// ...
// Here we are getting the first accounts from the signer
// accessing the address and renaming it to `signerAddress`
const [{ address : signerAddress }] = await signer.getAccounts();
// ...function makeSignDoc(msgs: AminoMsg[], fee: StdFee, chainId: string, memo: string | undefined, accountNumber: number | string, sequence: number | string, timeout_height?: bigint): StdSignDoc;type AminoMsg = {
// static type url
type: ;
value: {
// signer address
signer: string;
// plaintext or base64 encoded message
data: string;
}
}const data = "my message";
const signDoc = makeSignDoc(
[{ // list of amino messages
type: "sign/MsgSignData",
value: {
signer: signerAddress,
data: data
}
}],
{ gas: "0", amount: [] }, // StdFee
"", // chainId
"", // memo
0, // accountNumber
0 // sequence
// timeout_height
)const signRes = await signer.signAmino(signerAddress, signDoc);import { sha256, Random } from "@cosmjs/crypto"
import { fromBase64, toBase64, toAscii } from "@cosmjs/encoding";
import { chacha20_poly1305_seal, ecdh } from "@solar-republic/neutrino"
// this is a dependency of `@solar-republic/neutrino` so consider importing these methos
import { concat, json_to_bytes } from "@blake.regalia/belt";
// ...
// define
//
// `clientPrivateKey`
// `gatewayPublicKey`
// `signerAddress`
//
// like described above
// ...
const sharedKey = sha256(ecdh(clientPrivateKey, gatewayPublicKey))
// We also need to generate a one-time nonce, which can be done like this:
const nonce = Random.getBytes(12)
// Prepare a message for the action you want to take on the contract
const msg = ExecuteMsg { ...}
/// Defining payload structure idential to the Rust data structure
const payload : EncryptedPayload = {
// e.g, cosmos1...
user_address: signerAddress,
// uint8array -> base64 (-> Binary)
user_pubkey: toBase64(signerPubkey),
// e.g. "cosmos" from "cosmos1..."
hrp: signerAddress.split("1")[0],
// or to toBinary(msg) from `@cosmjs/cosmwasm-stargate`
msg: toBase64(json_to_bytes(msg))
}
/// getting the payload ciphertext
const ciphertext = concat(chacha20_poly1305_seal(
sharedKey,
nonce,
// or toUtf8( JSON.stringify(payload) )
json_to_bytes( payload )
));
// finally the payload_hash is sha256 of the ciphertext
const ciphertextHash = sha256(ciphertext);
// ...// calling `makeSignDoc` with nullifed fields like described earlier
const signDoc = getArb36SignDoc(signerAddress, ciphertextHash);
// signing the message
const signRes = await signer.signAmino(signerAddress, signDoc);const encrypted = {
// uint8array -> base64 (-> Binary)
nonce : toBase64(nonce),
// public key of a pair that was used in deriving a shread key
user_key : toBase64(clientPublicKey),
// ciphertext of with the user data and actual message
payload : toBase64(ciphertext),
// sha256 hash of the ciphertext
payload_hash : toBase64(ciphertextHash),
// signatire over sha256( getArb36SignDoc( payload_hash ) ) already in base64
payload_signature : signRes.signature.signature,
}import { MsgTransfer } from "cosmjs-types/ibc/applications/transfer/v1/tx";
import { SigningStargateClient, MsgTransferEncodeObject } from "@cosmjs/stargate";
/// creating a client
const client = await SigningStargateClient(
"https://rpc.cosmoshub.io" // endpoint of the remote network
signer, // offlineSigner
)
// defining the IBC transfer message
const msg : MsgTransferEncodeObject = {
typeUrl: "/ibc.applications.transfer.v1.MsgTransfer",
value: MsgTransfer.fromPartial({
sender: signerAddress,
receiver: secretGatewayContractAddress,
sourceChannel: "channel-0",
sourcePort: "transfer",
timeoutTimestamp: BigInt(
// 5 minutes from now | ms -> ns
Math.floor(Date.now() + 300_000) * 1_000_000
),
// IBC Hook memo msg
memo: JSON.stringify({
wasm: {
// must be same as receiver
contract: secretGatewayContractAddress,
// encrypted message defined above
msg: encrypted
}
})
})
}
// signing and broadcasting the message
const res = await client.signAndBroadcast(signerAddress, [msg])"wasm"{
//... other ibc fields omitted for example
"data": {
"denom": "denom on counterparty chain (e.g. uatom)", // will be transformed to the local denom (ibc/...)
"amount": "1000",
"sender": "addr on counterparty chain", // will be ignored and shown to the contract as a null sender (cannot be verifed over IBC)
"receiver": "secret1contractAddr",
"memo": {
"wasm": {
"contract": "secret1contractAddr",
"msg": {
"raw_message_fields": "raw_message_data"
}
}
}
}
}#[entry_point]
pub fn execute(_deps: DepsMut, env: Env, info: MessageInfo, msg: Msg) -> StdResult<Response> {
match msg {
Msg::WrapDeposit {
snip20_address,
snip20_code_hash,
recipient_address,
} => Ok(Response::default().add_messages(vec![
CosmosMsg::Wasm(cosmwasm_std::WasmMsg::Execute {
contract_addr: snip20_address.clone(),
code_hash: snip20_code_hash.clone(),
msg: to_binary(&secret_toolkit::snip20::HandleMsg::Deposit { padding: None })
.unwrap(),
funds: info.funds.clone(),
}),
CosmosMsg::Wasm(cosmwasm_std::WasmMsg::Execute {
contract_addr: snip20_address,
code_hash: snip20_code_hash,
msg: to_binary(&secret_toolkit::snip20::HandleMsg::Transfer {
recipient: recipient_address,
amount: info.funds[0].amount,
memo: None,
padding: None,
})
.unwrap(),
funds: vec![],
}),
])),
}
}#[cw_serde]
pub enum IBCLifecycleComplete {
#[serde(rename = "ibc_ack")]
IBCAck {
/// The source channel (secret side) of the IBC packet
channel: String,
/// The sequence number that the packet was sent with
sequence: u64,
/// String encoded version of the ack as seen by OnAcknowledgementPacket(..)
ack: String,
/// Weather an ack is a success of failure according to the transfer spec
success: bool,
},
#[serde(rename = "ibc_timeout")]
IBCTimeout {
/// The source channel (secret side) of the IBC packet
channel: String,
/// The sequence number that the packet was sent with
sequence: u64,
},
}
/// Message type for `sudo` entry_point
#[cw_serde]
pub enum SudoMsg {
#[serde(rename = "ibc_lifecycle_complete")]
IBCLifecycleComplete(IBCLifecycleComplete),
}
#[entry_point]
pub fn sudo(_deps: DepsMut, _env: Env, msg: SudoMsg) -> StdResult<Response> {
match msg {
SudoMsg::IBCLifecycleComplete(IBCLifecycleComplete::IBCAck {
channel,
sequence,
ack,
success,
}) => todo!(),
SudoMsg::IBCLifecycleComplete(IBCLifecycleComplete::IBCTimeout {
channel,
sequence,
}) => todo!(),
}
}/// A data structure that is safe to be visible by all network participants and can be transmited over non-secure channels
struct EncryptedParams {
/// Encrypted payload containging hidden message
pub payload : Binary,
/// Sha256 hash of the payload
pub payload_hash : Binary,
/// Signed base64 digest of the payload_hash being wrapped
/// in an cosmos arbitrary (036) object and rehashed again with sha256
pub payload_signature : Binary,
/// Public key of wallet used for deriving a shared key for chacha20_poly1305
/// Not necessary the same as user's public key
pub user_key : Binary,
/// One-time nonce used for chacha20_poly1305 encryption
pub nonce : Binary,
}
/// Data meant to be encrypted and stored in the payload field of [EncryptedParams]
#[cw_serde]
pub struct EncryptedPayload {
/// bech32 prefix address of a wallet used for signing hash of the payload
pub user_address : String,
/// Public key of a wallet used for signing hash of the payload
pub user_pubkey : Binary,
/// Human readable prefix for the bech32 address on the remote cosmos chain
pub hrp : String,
/// Plaintext message e.g. normal `ExecuteMsg` of your contract
pub msg : Binary,
}pub enum ExecuteMsg {
...
Encrypted {
payload : Binary,
payload_signature : Binary,
payload_hash : Binary,
user_key : Binary,
nonce : Binary,
}
...
}trait WithEncryption : Serialize + Clone {
fn encrypted(&self) -> EncryptedParams;
fn is_encrypted(&self) -> bool;
}impl WithEncryption for ExecuteMsg {
fn encrypted(&self) -> EncryptedParams {
match self.clone() {
ExecuteMsg::Encrypted {
payload,
payload_signature,
payload_hash,
user_key,
nonce,
} => EncryptedParams {
payload,
payload_signature,
payload_hash,
user_key,
nonce
},
_ => panic!("Not encrypted")
}
}
fn is_encrypted(&self) -> bool {
if ExecuteMsg::Encrypted{..} = self {
true
} else {
false
}
}
}pub enum GatewayExecuteMsg<E = Option<Empty>>
where E: JsonSchema
{
ResetEncryptionKey {} ,
Encrypted {
payload : Binary,
payload_signature : Binary,
payload_hash : Binary,
user_key : Binary,
nonce : Binary,
},
Extension {
msg : E
}
}// Degine your custom message
#[cw_serde]
pub enum MyCustomMessage {
HandleFoo {}
HandleBar {}
}
// Extend the GatewayExecuteMsg
pub type MyGatewayExecuteMsg = GatewayExecuteMsg<MyCustomMessage>;/// MyGatewayExecuteMsg
match msg {
...
ResetEncryptionKey => { ... },
MyGatewayExecuteMsg::Extension{msg} => {
/// MyCustomMessage
match msg {
MyCustomMessage::HandleFoo{} => {
// Do something
}
MyCustomMessage::HandleBar{} => {
// Do something
}
}
}
...
} let decrypted = wallet.decrypt_to_payload(
¶ms.payload,
¶ms.user_key,
¶ms.nonce,
)?;pub fn chacha20poly1305_decrypt(
ciphertext : &impl Deref<Target = [u8]>,
key : &impl Deref<Target = [u8]>,
nonce : &impl Deref<Target = [u8]>,
) -> StdResult<Vec<u8>> {
...
}fn verify_arbitrary<M : Display>(api: &dyn Api, cred: &CosmosCredential<M>) -> StdResult<String>pub struct CosmosCredential<M = String>
where M: Display
{
/// public key matching the respective secret that was used to sign message
pub pubkey : Binary,
/// signed sha256 digest of a message wrapped in arbitary data (036) object
pub signature : Binary,
/// signed inner message before being wrapped with 036
pub message : M,
/// prefix for the bech32 address on remote cosmos chain
pub hrp : String
}fn preamble_msg_arb_036(signer: &str, data: &str) -> String