From f1743e25b2b8f0174c7aeebe9ba3099d184c216b Mon Sep 17 00:00:00 2001 From: Lim Jet <57783762+daoauth@users.noreply.github.com> Date: Thu, 18 Jul 2024 20:04:57 +0900 Subject: [PATCH] update sui signer --- functions/src/index.ts | 15 ++--- functions/src/types.ts | 16 ++--- package-lock.json | 51 ++++++++++++++ package.json | 2 + src/component/Loading.tsx | 90 ++++++++++--------------- src/component/Provenance.tsx | 61 +++++++++++++---- src/component/chains/Sui.tsx | 92 ++++++++++++++++++++++++-- src/component/utils/getMoveObjectId.ts | 29 ++++++++ src/component/utils/parseMoveToml.ts | 27 ++++++++ src/index.tsx | 4 ++ 10 files changed, 297 insertions(+), 90 deletions(-) create mode 100644 src/component/utils/getMoveObjectId.ts create mode 100644 src/component/utils/parseMoveToml.ts diff --git a/functions/src/index.ts b/functions/src/index.ts index 7ac69d2..e568a42 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -140,12 +140,10 @@ export const update = onRequest({ cors: true }, async (req, res) => { return; } - const { uid, serializedSignedTx } = req.body; + const { uid, signedData } = req.body; - if (!uid || !serializedSignedTx) { - res - .status(400) - .send('Invalid input, missing "uid" or "serializedSignedTx"'); + if (!uid || !signedData || !signedData.message || !signedData.signature) { + res.status(400).send('Invalid input, missing "uid" or "signedData"'); return; } @@ -163,7 +161,7 @@ export const update = onRequest({ cors: true }, async (req, res) => { const newDocRef = firestore.collection('signed').doc(uid); await newDocRef.set({ ...data, - serializedSignedTx, + signedData, createdAt: admin.firestore.FieldValue.serverTimestamp(), }); @@ -194,15 +192,14 @@ const _load = async ( return; } - const { name, network, provenance, serializedSignedTx } = - doc.data() as DocData; + const { name, network, provenance, signedData } = doc.data() as DocData; if (collection === 'signed') { const storage = admin.storage(); const bucket = storage.bucket('slsa-on-blockchain.appspot.com'); const file = bucket.file(uid); await file.delete(); await docRef.delete(); - res.status(200).json({ name, network, serializedSignedTx }); + res.status(200).json({ name, network, signedData }); } else { res.status(200).json({ name, network, provenance }); } diff --git a/functions/src/types.ts b/functions/src/types.ts index 86d389e..bfec4fb 100644 --- a/functions/src/types.ts +++ b/functions/src/types.ts @@ -1,13 +1,11 @@ -interface Provenance { - summary: string; // build summary - commit: string; // source commit - workflow: string; // build workflow - ledger: string; // public ledger -} - export interface DocData { name: string; // packageName network: string; - provenance: Provenance; - serializedSignedTx?: string; + provenance: string; + signedData?: + | { + message: string; + signature: string; + } + | string; } diff --git a/package-lock.json b/package-lock.json index 3adee0f..f284dce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,11 +13,13 @@ "@radix-ui/themes": "^3.1.1", "@tanstack/react-query": "^5.51.1", "fflate": "^0.8.2", + "notistack": "^3.0.1", "query-string": "^9.0.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-scripts": "5.0.1", "recoil": "^0.7.7", + "smol-toml": "^1.2.2", "tar-stream": "^3.1.7", "web-vitals": "^2.1.4" }, @@ -10845,6 +10847,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/goober": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.14.tgz", + "integrity": "sha512-4UpC0NdGyAFqLNPnhCT2iHpza2q+RAY3GV85a/mRPdzyPQMsj0KmMMuetdIkzWRbJ+Hgau1EZztq8ImmiMGhsg==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -14700,6 +14711,37 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/notistack": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/notistack/-/notistack-3.0.1.tgz", + "integrity": "sha512-ntVZXXgSQH5WYfyU+3HfcXuKaapzAJ8fBLQ/G618rn3yvSzEbnOB8ZSOwhX+dAORy/lw+GC2N061JA0+gYWTVA==", + "license": "MIT", + "dependencies": { + "clsx": "^1.1.0", + "goober": "^2.0.33" + }, + "engines": { + "node": ">=12.0.0", + "npm": ">=6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/notistack" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/notistack/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -18006,6 +18048,15 @@ "node": ">=8" } }, + "node_modules/smol-toml": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.2.2.tgz", + "integrity": "sha512-fVEjX2ybKdJKzFL46VshQbj9PuA4IUKivalgp48/3zwS9vXzyykzQ6AX92UxHSvWJagziMRLeHMgEzoGO7A8hQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + } + }, "node_modules/sockjs": { "version": "0.3.24", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", diff --git a/package.json b/package.json index f5730b2..519ac3a 100644 --- a/package.json +++ b/package.json @@ -8,11 +8,13 @@ "@radix-ui/themes": "^3.1.1", "@tanstack/react-query": "^5.51.1", "fflate": "^0.8.2", + "notistack": "^3.0.1", "query-string": "^9.0.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-scripts": "5.0.1", "recoil": "^0.7.7", + "smol-toml": "^1.2.2", "tar-stream": "^3.1.7", "web-vitals": "^2.1.4" }, diff --git a/src/component/Loading.tsx b/src/component/Loading.tsx index 925e9e2..07a5562 100644 --- a/src/component/Loading.tsx +++ b/src/component/Loading.tsx @@ -13,7 +13,7 @@ export const Loading = () => { const initialized = useRef(false); const setState = useSetRecoilState(STATE); - const [message, setMessage] = useState('Initializing ....'); + const [message, setMessage] = useState(''); const [error, setError] = useState(false); const unzip = async ( @@ -56,61 +56,43 @@ export const Loading = () => { const { q: uid } = queryString.parse(window.location.search) as { q: string; }; - fetch('https://read-jx4b2hndxq-uc.a.run.app', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ uid }), - }) - .then((res1) => { - res1 - .json() - .then((data) => { - setMessage('Loading Data ....'); - fetch('https://download-jx4b2hndxq-uc.a.run.app', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ filename: uid }), - }) - .then((res2) => { - res2 - .text() - .then((gzip) => { - unzip(gzip) - .then((files) => { - setState({ - uid, - files, - data, - }); - }) - .catch((e) => { - setError(true); - setMessage(`${e}`); - }); - }) - .catch((e) => { - setError(true); - setMessage(`${e}`); - }); - }) - .catch((e) => { - setError(true); - setMessage(`${e}`); - }); - }) - .catch((e) => { - setError(true); - setMessage(`${e}`); - }); - }) - .catch((e) => { + const fetchData = async () => { + try { + setMessage('Initializing ....'); + const res1 = await fetch('https://read-jx4b2hndxq-uc.a.run.app', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ uid }), + }); + const data = await res1.json(); + + setMessage('Loading Data ....'); + const res2 = await fetch( + 'https://download-jx4b2hndxq-uc.a.run.app', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ filename: uid }), + }, + ); + const gzip = await res2.text(); + const files = await unzip(gzip); + setState({ + uid, + files, + data, + }); + } catch (e) { setError(true); setMessage(`${e}`); - }); + } + }; + + fetchData(); } else { setError(true); setMessage('Query params error'); diff --git a/src/component/Provenance.tsx b/src/component/Provenance.tsx index c980ad6..162504c 100644 --- a/src/component/Provenance.tsx +++ b/src/component/Provenance.tsx @@ -1,13 +1,44 @@ -import { Avatar, Box, Button, Card, Flex, Link, Text } from '@radix-ui/themes'; +import type { ReactElement } from 'react'; +import { useEffect, useState } from 'react'; + +import { fromB64 } from '@mysten/sui/utils'; +import { Avatar, Box, Card, Flex, Link, Text } from '@radix-ui/themes'; import { useRecoilState } from 'recoil'; import { STATE } from '../recoil'; -export const Provenance = () => { +interface GithubAction { + summary: string; // build summary + commit: string; // source commit + workflow: string; // build workflow + ledger: string; // public ledger +} + +export const Provenance = ({ BtnSign }: { BtnSign: ReactElement }) => { const [state] = useRecoilState(STATE); + const [gha, setGha] = useState(undefined); + + useEffect(() => { + if (state && !gha) { + const provenance = JSON.parse( + new TextDecoder().decode(fromB64(state.data.provenance)), + ); + const payload = JSON.parse( + new TextDecoder().decode(fromB64(provenance.payload)), + ); + setGha({ + summary: `https://github.com/zktx-io/move_on_github_action/actions/runs/${payload.predicate.invocation.environment.github_run_id}/attempts/${payload.predicate.invocation.environment.github_run_attempt}`, + commit: `https://github.com/zktx-io/move_on_github_action/tree/${payload.predicate.invocation.environment.github_sha1}`, + workflow: `https://github.com/zktx-io/move_on_github_action/actions/runs/${payload.predicate.invocation.environment.github_run_id}/workflow`, + ledger: `https://search.sigstore.dev/?hash=${payload.subject[0].digest.sha256}`, + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [state]); + return ( <> - {state && ( + {state && gha && ( @@ -32,7 +63,7 @@ export const Provenance = () => { - Built and signed on Github Actions + Building and Deploying with GitHub Actions @@ -40,10 +71,11 @@ export const Provenance = () => { - {state.data.provenance.summary} + {gha.summary} @@ -52,10 +84,11 @@ export const Provenance = () => { - {state.data.provenance.commit} + {gha.commit} @@ -64,10 +97,11 @@ export const Provenance = () => { - {state.data.provenance.workflow} + {gha.workflow} @@ -76,15 +110,16 @@ export const Provenance = () => { - {state.data.provenance.ledger} + {gha.ledger} - + {BtnSign} diff --git a/src/component/chains/Sui.tsx b/src/component/chains/Sui.tsx index 96abb38..39a7855 100644 --- a/src/component/chains/Sui.tsx +++ b/src/component/chains/Sui.tsx @@ -1,13 +1,87 @@ -import { ConnectButton, useCurrentAccount } from '@mysten/dapp-kit'; -import { Flex } from '@radix-ui/themes'; +import { useState } from 'react'; + +import { + ConnectButton, + useCurrentAccount, + useSignTransaction, + useSuiClientContext, +} from '@mysten/dapp-kit'; +import { Transaction } from '@mysten/sui/transactions'; +import { normalizeSuiObjectId, toB64 } from '@mysten/sui/utils'; +import { Button, Flex } from '@radix-ui/themes'; +import { enqueueSnackbar } from 'notistack'; import { useRecoilState } from 'recoil'; import { STATE } from '../../recoil'; import { Provenance } from '../Provenance'; +import { getMoveObjectId } from '../utils/getMoveObjectId'; +import { parseMoveToml } from '../utils/parseMoveToml'; export const Sui = () => { - const currentAccount = useCurrentAccount(); + const ctx = useSuiClientContext(); + const { mutateAsync: signTransaction } = useSignTransaction(); + const account = useCurrentAccount(); const [state] = useRecoilState(STATE); + const [disabled, setDisabled] = useState(false); + + const handleSign = async () => { + if (state && account) { + setDisabled(true); + const network = state.data.network.split('/')[1]; + ctx.selectNetwork(network); + const { dependencies } = parseMoveToml(state.files['./Move.toml']); + const ids = await getMoveObjectId({ + MoveStdlib: { + git: 'https://github.com/MystenLabs/sui.git', + rev: 'framework/testnet', + subdir: 'crates/sui-framework/packages/move-stdlib', + }, + ...dependencies, + }); + const regex = new RegExp( + `^\\.\\/build\\/${state.data.name}\\/bytecode_modules\\/[^\\/]+\\.mv$`, + ); + const files = Object.keys(state.files).filter((name) => regex.test(name)); + const modules = files.map((name) => toB64(state.files[name])); + const transaction = new Transaction(); + transaction.transferObjects( + [ + transaction.publish({ + modules, + dependencies: ids.map((item) => normalizeSuiObjectId(item)), + }), + ], + account.address, + ); + + try { + const { bytes, signature } = await signTransaction({ + transaction, + chain: `sui:${network}`, + }); + console.log({ bytes, signature }); // TODO + await fetch('https://update-jx4b2hndxq-uc.a.run.app', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + uid: state.uid, + signedData: { + transaction: bytes, + signature, + }, + }), + }); + } catch (e: any) { + enqueueSnackbar(e.message, { + variant: 'error', + }); + } finally { + setDisabled(false); + } + } + }; return ( { justify="center" height="100vh" > - {state && currentAccount && } - {!currentAccount && } + {state && account && ( + + Sign + + } + /> + )} + {!account && } ); }; diff --git a/src/component/utils/getMoveObjectId.ts b/src/component/utils/getMoveObjectId.ts new file mode 100644 index 0000000..6edef99 --- /dev/null +++ b/src/component/utils/getMoveObjectId.ts @@ -0,0 +1,29 @@ +import { parseMoveToml } from './parseMoveToml'; + +import type { IMoveDependency } from './parseMoveToml'; + +export const getMoveObjectId = async (dependencies: { + [key: string]: IMoveDependency; +}): Promise => { + const addresses: string[] = []; + const temp = await Promise.all( + Object.keys(dependencies).map(async (item): Promise => { + const { git, rev, subdir } = dependencies[item]; + const url = `${git.replace(/\.git$/, '').replace('https://github.com', 'https://raw.githubusercontent.com')}/${rev}/${subdir}/Move.toml`; + const res = await fetch(url); + const moveToml = await res.text(); + const { addresses: packageIds } = parseMoveToml(moveToml) as { + addresses: { [key: string]: string }; + }; + const list: string[] = []; + for (const id of Object.keys(packageIds)) { + list.push(packageIds[id]); + } + return list; + }), + ); + for (const item of temp) { + addresses.push(...item); + } + return addresses; +}; diff --git a/src/component/utils/parseMoveToml.ts b/src/component/utils/parseMoveToml.ts new file mode 100644 index 0000000..e51e3cd --- /dev/null +++ b/src/component/utils/parseMoveToml.ts @@ -0,0 +1,27 @@ +import { parse } from 'smol-toml'; + +export interface IMoveDependency { + git: string; + subdir: string; + rev: string; +} + +export const parseMoveToml = (toml: string | Uint8Array) => { + try { + return parse( + new TextDecoder().decode( + typeof toml === 'string' ? new TextEncoder().encode(toml) : toml, + ), + ) as unknown as { + package: { + edition: string; + name: string; + version: string; + }; + addresses: { [key: string]: string }; + dependencies: { [key: string]: IMoveDependency }; + }; + } catch (error) { + throw new Error(`${error}`); + } +}; diff --git a/src/index.tsx b/src/index.tsx index bc91784..8489744 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { Theme } from '@radix-ui/themes'; +import { SnackbarProvider } from 'notistack'; import { createRoot } from 'react-dom/client'; import { RecoilRoot } from 'recoil'; @@ -15,6 +16,9 @@ root.render( +