From a7533de258140bc5e93317e6dcf6090a80139a2b Mon Sep 17 00:00:00 2001 From: Jeff Eganhouse Date: Thu, 12 Sep 2024 20:13:09 -0400 Subject: [PATCH 1/8] Update the front end to not need to interact with the backend --- gala-faucet-frontend/package-lock.json | 34 ++++- gala-faucet-frontend/package.json | 8 +- gala-faucet-frontend/src/App.vue | 4 +- .../src/components/Balance.vue | 24 +++- .../src/components/BurnGala.vue | 119 ++++++++++++++---- 5 files changed, 150 insertions(+), 39 deletions(-) diff --git a/gala-faucet-frontend/package-lock.json b/gala-faucet-frontend/package-lock.json index 6582758..4ecce05 100644 --- a/gala-faucet-frontend/package-lock.json +++ b/gala-faucet-frontend/package-lock.json @@ -11,9 +11,14 @@ "@gala-chain/connect": "^1.4.2", "axios": "^0.27.2", "bignumber.js": "^9.0.2", + "bn.js": "^5.2.1", + "elliptic": "^6.5.4", + "js-sha3": "^0.8.0", + "json-stringify-deterministic": "^1.0.8", "vue": "^3.3.4" }, "devDependencies": { + "@types/elliptic": "^6.4.18", "@types/node": "^18.11.9", "@vitejs/plugin-vue": "^4.2.3", "@vue/compiler-sfc": "^3.3.4", @@ -511,6 +516,11 @@ } } }, + "node_modules/@gala-chain/api/node_modules/js-sha3": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.9.3.tgz", + "integrity": "sha512-BcJPCQeLg6WjEx3FE591wVAevlli8lxsxm9/FzV4HXkV49TmBH38Yvrpce6fjbADGMKFrBMGTqrVz3qPIZ88Gg==" + }, "node_modules/@gala-chain/api/node_modules/tslib": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", @@ -628,6 +638,24 @@ "node": ">= 8" } }, + "node_modules/@types/bn.js": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.5.tgz", + "integrity": "sha512-V46N0zwKRF5Q00AZ6hWtN0T8gGmDUaUzLWQvHFo5yThtVwK/VCenFY3wXVbOvNfajEpsTfQM4IN9k/d6gUVX3A==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/elliptic": { + "version": "6.4.18", + "resolved": "https://registry.npmjs.org/@types/elliptic/-/elliptic-6.4.18.tgz", + "integrity": "sha512-UseG6H5vjRiNpQvrhy4VF/JXdA3V/Fp5amvveaL+fs28BZ6xIKJBPnUPRlEaZpysD9MbpfaLi8lbl7PGUAkpWw==", + "dev": true, + "dependencies": { + "@types/bn.js": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2093,9 +2121,9 @@ "dev": true }, "node_modules/js-sha3": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.9.3.tgz", - "integrity": "sha512-BcJPCQeLg6WjEx3FE591wVAevlli8lxsxm9/FzV4HXkV49TmBH38Yvrpce6fjbADGMKFrBMGTqrVz3qPIZ88Gg==" + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" }, "node_modules/js-yaml": { "version": "4.1.0", diff --git a/gala-faucet-frontend/package.json b/gala-faucet-frontend/package.json index 7beb28a..a9c3102 100644 --- a/gala-faucet-frontend/package.json +++ b/gala-faucet-frontend/package.json @@ -13,8 +13,12 @@ "dependencies": { "@gala-chain/connect": "^1.4.2", "axios": "^0.27.2", - "vue": "^3.3.4", - "bignumber.js": "^9.0.2" + "bignumber.js": "^9.0.2", + "bn.js": "^5.2.1", + "@types/elliptic": "^6.4.18", + "js-sha3": "^0.8.0", + "json-stringify-deterministic": "^1.0.8", + "vue": "^3.3.4" }, "devDependencies": { "@types/node": "^18.11.9", diff --git a/gala-faucet-frontend/src/App.vue b/gala-faucet-frontend/src/App.vue index 46648d8..f6e5d7a 100644 --- a/gala-faucet-frontend/src/App.vue +++ b/gala-faucet-frontend/src/App.vue @@ -6,8 +6,8 @@
- - + +
import { ref, onMounted, watch, computed } from 'vue' import axios from 'axios' +import { MetamaskConnectClient } from '@gala-chain/connect' const props = defineProps<{ network: 'mainnet' | 'testnet' walletAddress: string + metamaskClient: MetamaskConnectClient | null }>() const balance = ref(null) @@ -21,14 +23,25 @@ const error = ref('') const networkName = computed(() => props.network === 'mainnet' ? 'Mainnet' : 'Testnet') const fetchBalance = async () => { - if (!props.walletAddress) return + if (!props.walletAddress || !props.metamaskClient) return + const apiBaseUrl = props.network === 'mainnet' + ? import.meta.env.VITE_MAINNET_API + : import.meta.env.VITE_TESTNET_API; try { error.value = '' - const response = await axios.get(`${import.meta.env.VITE_APP_API_URL}/getBalance`, { - params: { walletAddress: props.walletAddress, network: props.network } - }) - balance.value = response.data + const balanceDto = { + owner: props.walletAddress, + collection: "GALA", + category: "Unit", + type: "none", + additionalKey: "none", + instance: "0" + } + + const response = await axios.post(`${apiBaseUrl}/api/asset/token-contract/FetchBalances`, balanceDto); + // TODO: more elegant checking of the balance response (0 items, locked amount, etc.) + balance.value = parseFloat(response.data.Data[0].quantity) } catch (err) { console.error(`Error fetching ${props.network} balance:`, err) error.value = `Error fetching ${props.network} balance. Please try again.` @@ -37,6 +50,7 @@ const fetchBalance = async () => { } watch(() => props.walletAddress, fetchBalance) +watch(() => props.metamaskClient, fetchBalance) onMounted(fetchBalance) diff --git a/gala-faucet-frontend/src/components/BurnGala.vue b/gala-faucet-frontend/src/components/BurnGala.vue index 8af0b6a..a5925c6 100644 --- a/gala-faucet-frontend/src/components/BurnGala.vue +++ b/gala-faucet-frontend/src/components/BurnGala.vue @@ -11,6 +11,13 @@ import { ref } from 'vue' import axios from 'axios' import { MetamaskConnectClient } from '@gala-chain/connect' +import stringify from 'json-stringify-deterministic'; +import { ec as EC } from 'elliptic'; +import { keccak256 } from 'js-sha3'; +import { BN } from 'bn.js'; +import { Buffer } from 'buffer'; + +const ecSecp256k1 = new EC('secp256k1'); const props = defineProps<{ isConnected: boolean @@ -25,36 +32,94 @@ const burnMessage = ref('') const burnGala = async () => { if (!props.metamaskClient) { burnMessage.value = 'Wallet not connected' - } else { - try { - const burnTokensDto = { - owner: `eth|${props.metamaskClient.getWalletAddress.slice(2)}`, - tokenInstances: [{ - quantity: amount.value, - tokenInstanceKey: { - collection: "GALA", - category: "Unit", - type: "none", - additionalKey: "none", - instance: "0" - } - }], - uniqueKey: `galaswap-operation-testnet-faucet-burn-${Date.now()}` - } - - const signedDto = await props.metamaskClient.sign("BurnTokens", burnTokensDto) - - // console.log("Signed", signedDto) - const response = await axios.post(`${import.meta.env.VITE_APP_API_URL}/burnMainnetGala`, signedDto) - burnMessage.value = `Successfully burned ${amount.value} GALA` - emit('burnSuccess') - amount.value = '' - } catch (error) { - console.error('Error burning GALA:', error) - burnMessage.value = 'Error burning GALA. Please try again.' + return + } + + try { + const owner = `eth|${props.metamaskClient.getWalletAddress.slice(2)}` + const burnAmount = amount.value + + // Burn on mainnet + const burnTokensDto = { + owner, + tokenInstances: [{ + quantity: burnAmount, + tokenInstanceKey: { + collection: "GALA", + category: "Unit", + type: "none", + additionalKey: "none", + instance: "0" + } + }], + uniqueKey: `galaswap-operation-testnet-faucet-burn-${Date.now()}` } + + const signedBurnDto = await props.metamaskClient.sign("BurnTokens", burnTokensDto) + await axios.post(`${import.meta.env.VITE_MAINNET_API}/api/asset/token-contract/BurnTokens`, signedBurnDto) + + // Mint on testnet + const mintAmount = parseFloat(burnAmount) * Number(import.meta.env.VITE_FAUCET_MULTIPLIER) + const mintTokensDto = { + owner: owner, + quantity: mintAmount.toString(), + tokenClass: { + collection: "GALA", + category: "Unit", + type: "none", + additionalKey: "none" + }, + tokenInstance: "0", + uniqueKey: `mint-${signedBurnDto.uniqueKey}` + } + + // sign with faucet admin credentials + const signedMintTokensDto = signObject(mintTokensDto, import.meta.env.VITE_FAUCET_ADMIN_PRIVATE_KEY) + const mintResponse = await axios.post(`${import.meta.env.VITE_TESTNET_API}/api/asset/token-contract/MintToken`, signedMintTokensDto) + + burnMessage.value = `Successfully burned ${burnAmount} GALA on mainnet and minted ${mintAmount} GALA on testnet` + emit('burnSuccess') + amount.value = '' + } catch (error) { + console.error('Error burning/minting GALA:', error) + burnMessage.value = 'Error burning/minting GALA. Please try again.' } } + +function signObject( + obj: TInputType, + privateKey: string +): TInputType & { signature: string } { + const toSign = { ...obj }; + + if ('signature' in toSign) { + delete toSign.signature; + } + + const stringToSign = stringify(toSign); + const stringToSignBuffer = Buffer.from(stringToSign); + + const keccak256Hash = Buffer.from(keccak256.digest(stringToSignBuffer)); + const privateKeyBuffer = Buffer.from(privateKey.replace(/^0x/, ''), 'hex'); + + const signature = ecSecp256k1.sign(keccak256Hash, privateKeyBuffer); + + // Normalize the signature if it's greater than half of order n + if (signature.s.cmp(ecSecp256k1.curve.n.shrn(1)) > 0) { + const curveN = ecSecp256k1.curve.n; + const newS = new BN(curveN).sub(signature.s); + const newRecoverParam = signature.recoveryParam != null ? 1 - signature.recoveryParam : null; + signature.s = newS; + signature.recoveryParam = newRecoverParam; + } + + const signatureString = Buffer.from(signature.toDER()).toString('base64'); + + return { + ...toSign, + signature: signatureString, + }; +}