Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ilariae/sui workshop #143

Draft
wants to merge 23 commits into
base: main
Choose a base branch
from
15 changes: 15 additions & 0 deletions .snippets/code/tutorials/messaging/loyalty/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const STATE_OBJ =
"0x31358d198147da50db32eda2562951d53973a0c0ad5ed738e9b17d88b213d790";
export const WORMHOLE_PKG_ID =
"0xf47329f4344f3bf0f8e436e2f7b485466cff300f12a166563995d3888c296a94";

// IMPORTANT: Use your own info here otherwise you will get errors.
export const PKG_ID =
"0xa51e87b6382c9d190df08eccbf89533d398ba45040b47668643a68f9b2cac435";
export const DATA_OBJ =
"0x9d8304dea2a5d6f0e7b52c19bff120d6006f4b5797e6702ed276202432cf1c1b";
export const EMITTER_CAP = "0x4bde3378c8d53f114d6b1ddba8996d1a3b786f62c568a8f22e952ef50b034e50";
export const SOLANA_PUBLIC_KEY = [
198, 68, 42, 48, 102, 139, 117, 182, 49, 37, 150, 238, 31, 140, 254, 233, 148,
45, 182, 106, 7, 90, 139, 199, 219, 215, 201, 252, 224, 252, 210, 139,
];
74 changes: 74 additions & 0 deletions .snippets/code/tutorials/messaging/loyalty/loyalty.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#[allow(implicit_const_copy)]
module loyalty_contracts::loyalty;

use sui::table::{Self, Table};

// errors
const EInexistentUser: u64 = 1;
const EWrongSigner: u64 = 2;


public struct LoyaltyData has key {
id: UID,
user_points: Table<vector<u8>, u64>,
admin_address: address,
}

fun init(ctx: &mut TxContext) {
let data = LoyaltyData {
id: object::new(ctx),
user_points: table::new<vector<u8>, u64>(ctx),
admin_address: ctx.sender(),
};
transfer::share_object(data);
}

public fun change_admin_address(
data: &mut LoyaltyData,
new_admin: address,
ctx: &mut TxContext,
) {
assert!(ctx.sender() == data.admin_address, EWrongSigner);
data.admin_address = new_admin;
}


public(package) fun add_points(
data: &mut LoyaltyData,
user: vector<u8>,
loyalty_points: u64,
) {
if (loyalty_points > 0) {
if(data.user_points.contains(user)) {
*data.user_points.borrow_mut(user) = *data.user_points.borrow(user) + loyalty_points;
} else {
data.user_points.add(user, loyalty_points);
};
};
}

public(package) fun remove_points(
data: &mut LoyaltyData,
user: vector<u8>,
loyalty_points: u64,
) {
assert!(data.user_points.contains(user), EInexistentUser);
let current_points = *data.user_points.borrow(user);
if(current_points > loyalty_points) {
*data.user_points.borrow_mut(user) = current_points - loyalty_points;
} else {
*data.user_points.borrow_mut(user) = 0;
};
}

// getters

public fun points(data: &LoyaltyData, user: vector<u8>): u64 {
*data.user_points.borrow(user)
}


#[test_only]
public fun init_for_tests(ctx: &mut TxContext) {
init(ctx);
}
100 changes: 100 additions & 0 deletions .snippets/code/tutorials/messaging/loyalty/messages.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
module loyalty_contracts::messages;

use sui::bcs;
use sui::coin::Coin;
use sui::clock::Clock;
use sui::sui::SUI;

use wormhole::vaa::{Self, VAA};
use wormhole::state::State;
use wormhole::publish_message::{Self, MessageTicket};
use wormhole::emitter::EmitterCap;

use loyalty_contracts::loyalty::{Self, LoyaltyData};

const EInvalidPayload: u64 = 404;
const EInexistentOpCode: u64 = 407;

public fun receive_message (
raw_vaa: vector<u8>,
wormhole_state: &State,
clock: &Clock,
data: &mut LoyaltyData
)
{

let vaa: VAA = vaa::parse_and_verify(wormhole_state, raw_vaa, clock);
let payload = vaa::take_payload(vaa);
let (op, _chain, user, amount) = parse_payload(payload);

// gain points
if (op == 0) {
loyalty::add_points(data, user, amount);
}
// use points
else if (op == 1) {
loyalty::remove_points(data, user, amount);
} else {
abort(EInexistentOpCode)
}

}

// suggested way is to call wormhole::publish_message::publish_message in a PTB
public fun new_message(
data: &LoyaltyData,
user: vector<u8>,
nonce: u32,
emitter_cap: &mut EmitterCap
): MessageTicket
{
let payload = prepare_payload(data.points(user), user);
publish_message::prepare_message(emitter_cap, nonce, payload)
}

// not suggested, useful_for tests
public fun emit_message_(
wormhole_state: &mut State,
message_fee: Coin<SUI>,
clock: &Clock,
data: &LoyaltyData,
user: vector<u8>,
nonce: u32,
emitter_cap: &mut EmitterCap
)
{
let msg_ticket = new_message(data, user, nonce, emitter_cap);
publish_message::publish_message(wormhole_state, message_fee, msg_ticket, clock);
}

// The emitted messages will contain the points a user has
fun prepare_payload(points: u64, user: vector<u8>): vector<u8> {
let mut payload: vector<u8> = bcs::to_bytes(&points);
vector::append(&mut payload, user);
payload
}


// We expect a payload to be an array of u8 types
// The last element is the operation (spend or consume)
// Then the second to last is a u8 denoting the chain
// Next 32 u8's are the public key
// First 8 are bcs encoded u64 value of the points gained
fun parse_payload(mut payload: vector<u8>): (u8, u8, vector<u8>, u64) {
let operation = vector::pop_back(&mut payload);
let chain = vector::pop_back(&mut payload);
let mut user_address: vector<u8> = vector[];
let mut i = 0;
while(i < 32) {
vector::push_back(&mut user_address, vector::pop_back(&mut payload));
i = i + 1;
};
// we wrote it backwards
vector::reverse(&mut user_address);
// We expect 8 u8s in the payload since a u64 encoded in bcs is 8 bytes.
assert!(vector::length(&payload) == 8, EInvalidPayload);

let mut amount_bcs = bcs::new(payload);
let amount = amount_bcs.peel_u64();
(operation, chain, user_address, amount)
}
178 changes: 178 additions & 0 deletions .snippets/code/tutorials/messaging/loyalty/scripts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { Wormhole, wormhole, signSendWait } from "@wormhole-foundation/sdk";
import solana from "@wormhole-foundation/sdk/solana";
import sui from "@wormhole-foundation/sdk/sui";
import bs58 from "bs58";
import { Transaction } from "@mysten/sui/transactions";
import * as fs from "fs";
import * as path from "path";
import {
DATA_OBJ,
EMITTER_CAP,
PKG_ID,
SOLANA_PUBLIC_KEY,
STATE_OBJ,
WORMHOLE_PKG_ID,
} from "./constants";
import { getSigner, client } from "./utils";
import { bcs } from "@mysten/sui/bcs";

const emit_sol_msg = async () => {
const sol = await solana();
const wh = await wormhole("Testnet", [solana]);
const chain = wh.getChain("Solana");
const rpc = await chain.getRpc();
const signer = await sol.getSigner(
rpc,
"2HBbkiwYdnuXzLMVxVCHCut962HQVFDrba4x4MLCAQCJqbFV3WKACPoN4hV1byM1dv6rsVP2LZmcC2bxqRsYyw5U",
);
const cha = Wormhole.chainAddress(chain.chain, signer.address());
const coreBridge = await chain.getWormholeCore();

const amountBcs = [40, 10, 0, 0, 0, 0, 0, 0];
const userAddress = Array.from(bs58.decode(signer.address()));
const chain_u8 = 0;
const operation = 0;
const payload = Uint8Array.from(
amountBcs.concat(userAddress).concat(chain_u8).concat(operation),
);
const publishTxs = coreBridge.publishMessage(
//Emmiter
cha.address,
// Payload
// encoding.bytes.encode('USD:500'),
payload,
// Nonce (user defined)
3301,
// ConsistencyLevel
0,
);

const txids = await signSendWait(chain, publishTxs, signer);
console.dir(txids, { depth: 5 });
const txid = txids[0].txid;
const [whm] = await chain.parseTransaction(txid);
// const [whm] = await chain.parseTransaction("4LUumUVtjhSioeRjFhSVjVeMBnnhBWYsgrqvj4bNJTSNW1Vf4GiRwzz2GEQ85UifPUtm23eMit4PNCooZ93kJ7ot");
const vaa = await wh.getVaa(whm!, "Uint8Array", 60_000);
console.log(vaa);
const vaaBuf = await wh.getVaaBytes(whm!, 60_000);
const filePath = path.join(__dirname, "vaa_result.json");
fs.writeFileSync(filePath, JSON.stringify(Array.from(vaaBuf!)));
return vaaBuf;
};

const updateSuiContract = async (vaa: Uint8Array) => {
const signer = getSigner();
const tx = new Transaction();
tx.moveCall({
target: `${PKG_ID}::messages::receive_message`,
arguments: [
tx.pure.vector("u8", Array.from(vaa)),
tx.object(STATE_OBJ),
tx.object("0x6"), // Clock
tx.object(DATA_OBJ),
],
});

const response = client.signAndExecuteTransaction({
transaction: tx,
signer,
options: {
showEffects: true,
},
});

console.log(response);
};

const getEmitterCap = async () => {
const signer = getSigner();
const tx = new Transaction();
const cap = tx.moveCall({
target: `${WORMHOLE_PKG_ID}::emitter::new`,
arguments: [tx.object(STATE_OBJ)],
});
tx.transferObjects([cap], tx.pure.address(signer.toSuiAddress()));

const response = await client.signAndExecuteTransaction({
transaction: tx,
signer,
options: {
showEffects: true,
showObjectChanges: true,
},
});

// We expect a single created
const change: any = response.objectChanges?.filter(
item => item.type === "created",
);
console.log(change[0].objectId);
};

const sendSuiMsg = async () => {
const nonce = 17;
const signer = getSigner();
const tx = new Transaction();
const msg = tx.moveCall({
target: `${PKG_ID}::messages::new_message`,
arguments: [
tx.object(DATA_OBJ),
tx.pure.vector("u8", SOLANA_PUBLIC_KEY),
tx.pure.u32(nonce),
tx.object(EMITTER_CAP),
],
});
const fee = tx.moveCall({
target: "0x2::coin::zero",
typeArguments: ["0x2::sui::SUI"],
});
tx.moveCall({
target: `${WORMHOLE_PKG_ID}::publish_message::publish_message`,
arguments: [
tx.object(STATE_OBJ),
fee,
msg,
tx.object("0x6"), // clock object
],
});

const response = await client.signAndExecuteTransaction({
transaction: tx,
signer,
options: {
showEffects: true,
showEvents: true,
},
});

return response.digest;
};

const readSuiMsg = async (txDigest: string) => {
const wh = await wormhole("Testnet", [sui]);
const chain = wh.getChain("Sui");
const [whm] = await chain.parseTransaction(
txDigest,
);
const vaa = await wh.getVaa(whm!, "Uint8Array", 60_000);
// console.log(vaa);
const pointsBcs = Array.from(vaa?.payload || []).slice(0,8);
// transforming bcs to number, this can be done with @mysten/sui/bcs
const points = bcs.U64.parse(Uint8Array.from(pointsBcs));
console.log(points === "2600");
};

// receiveMsg()
const receiveMsg = async () => {
// Mock solana send
// Here we have hardcoded the payload to add 2600 points to the solana public key we have in constants.ts
const result = await emit_sol_msg();
if (result) updateSuiContract(result);
};

const sendMsg = async () => {
const txDigest = await sendSuiMsg();
await readSuiMsg(txDigest);
};

// sendMsg()
18 changes: 18 additions & 0 deletions .snippets/code/tutorials/messaging/loyalty/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
import { decodeSuiPrivateKey } from "@mysten/sui/cryptography";
import * as dotenv from "dotenv";
import { getFullnodeUrl, SuiClient } from "@mysten/sui/client";
dotenv.config();

export const getSigner = () => {
const { schema, secretKey } = decodeSuiPrivateKey(process.env.SUI_SK!);
if (schema !== "ED25519") {
throw `Expected ED25119 type, but got ${schema}. Remember that Sui allows for 3 types of keys`;
}
let signer = Ed25519Keypair.fromSecretKey(secretKey);
return signer;
};

export const client = new SuiClient({
url: getFullnodeUrl("testnet")
});
2 changes: 1 addition & 1 deletion tutorials/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ This section contains step-by-step tutorials to help you build with Wormhole.

[:octicons-arrow-right-16: Start building](/docs/tutorials/messaging/)

- :octicons-checklist-16:{ .lg .middle } **Messaging**
- :octicons-checklist-16:{ .lg .middle } **MultiGov**

---

Expand Down
Loading