diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0127220 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.idea/ +*.iml +target/ +node_modules/ \ No newline at end of file diff --git a/README.md b/README.md index 1333ed7..de8065b 100644 --- a/README.md +++ b/README.md @@ -1 +1,28 @@ +# Defi UI TODO + TODO + +Demo for how to integrate PBC wallet into a web frontend or dApp. + +## Requirements + +To be able to run the demo the following setup is required. + +- node.js version v.16.15.0 or newer. + +## How to run? + +First type check the Typescript using. + +```shell +npm ts +``` + +To run the example run + +```shell +npm install +npm start +``` + +and view the demo at localhost:8080 diff --git a/package.json b/package.json new file mode 100644 index 0000000..899eee3 --- /dev/null +++ b/package.json @@ -0,0 +1,56 @@ +{ + "name": "ui-integration-demo", + "version": "0.10.0", + "description": "", + "license": "AGPL-3.0", + "dependencies": { + "@ledgerhq/hw-transport": "^6.30.3", + "@ledgerhq/hw-transport-webusb": "^6.28.3", + "@ledgerhq/logs": "^6.12.0", + "@partisiablockchain/abi-client": "^3.30.0", + "@secata-public/bitmanipulation-ts": "^3.0.6", + "@types/bn.js": "^5.1.1", + "@types/elliptic": "^6.4.14", + "bn.js": "^5.2.1", + "buffer": "^6.0.3", + "crypto-browserify": "^3.12.0", + "elliptic": "^6.5.4", + "hash.js": "^1.1.7", + "partisia-blockchain-applications-sdk": "^0.1.2", + "stream-browserify": "^3.0.0", + "bip32-path": "^0.4.2" + }, + "devDependencies": { + "@babel/plugin-transform-runtime": "^7.19.6", + "@babel/preset-env": "^7.19.4", + "@babel/preset-typescript": "^7.18.6", + "@typescript-eslint/eslint-plugin": "^7.0.0", + "assert": "^2.0.0", + "babel-loader": "^9.1.0", + "eslint": "^8.14.0", + "eslint-plugin-import": "^2.26.0", + "fork-ts-checker-webpack-plugin": "^9.0.0", + "html-webpack-plugin": "^5.5.0", + "prettier": "^3.0.0", + "process": "^0.11.10", + "typescript": "^5.0.2", + "webpack": "^5.77.0", + "webpack-cli": "^5.0.0", + "webpack-dev-server": "^4.11.1", + "webpack-merge": "^5.8.0" + }, + "overrides": { + "merkletreejs": "^0.3.11" + }, + "scripts": { + "test": "npx webpack --config webpack.config.js", + "prettier": "npx prettier --print-width 100 --tab-width 2 --quote-props as-needed --trailing-comma es5 --bracket-same-line --prose-wrap preserve --write src", + "lint": "npx eslint src --max-warnings 0", + "start": "npx webpack serve --open --env PORT=8080" + }, + "prettier": { + "printWidth": 100, + "endOfLine": "auto", + "bracketSameLine": true + } +} diff --git a/src/main/abi/MpcToken.ts b/src/main/abi/MpcToken.ts new file mode 100644 index 0000000..f5ccd88 --- /dev/null +++ b/src/main/abi/MpcToken.ts @@ -0,0 +1,398 @@ +/* + * Copyright (C) 2022 - 2023 Partisia Blockchain Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import BN from "bn.js"; +import { + AbiParser, + AbstractBuilder, + BigEndianReader, + FileAbi, + FnKinds, + FnRpcBuilder, + RpcReader, + ScValue, + ScValueEnum, + ScValueOption, + ScValueStruct, + StateReader, + TypeIndex, + StateBytes, + BlockchainAddress, + Hash, +} from "@partisiablockchain/abi-client"; +import { BigEndianByteOutput } from "@secata-public/bitmanipulation-ts"; + +const fileAbi: FileAbi = new AbiParser( + Buffer.from( + "5042434142490900000501000000000701000000154d7063546f6b656e436f6e74726163745374617465000000040000000c696365645374616b696e6773120f1213120001000000066c6f636b65640c000000107374616b6544656c65676174696f6e73120f1213120002000000097472616e7366657273120f1213120006010000000a4963655374616b696e670000000300000006616d6f756e740900000008636f6e7472616374120d0000000673656e646572120d010000000f5374616b6544656c65676174696f6e0000000400000006616d6f756e74090000000e64656c65676174696f6e5479706512000300000009726563697069656e74120d0000000673656e646572120d020000000e44656c65676174696f6e5479706500000002000004010005010000001e44656c65676174696f6e547970652444454c45474154455f5354414b455300000000010000002744656c65676174696f6e5479706524524554524143545f44454c4547415445445f5354414b45530000000001000000135472616e73666572496e666f726d6174696f6e0000000400000006616d6f756e74121800000009726563697069656e74120d0000000673656e646572120d0000000673796d626f6c120b000000170100000006637265617465ffffffff0f00000001000000066c6f636b65640c020000000b7374616b65546f6b656e73000000000100000006616d6f756e7409020000000d756e7374616b65546f6b656e73010000000100000006616d6f756e740902000000166469736173736f6369617465466f7252656d6f76656402000000010000000f636f6e7472616374416464726573730d02000000087472616e73666572030000000200000009726563697069656e740d00000006616d6f756e7409020000000561626f727404000000010000000d7472616e73616374696f6e4964130200000014636865636b50656e64696e67556e7374616b657305000000000200000011636865636b566573746564546f6b656e73060000000002000000157472616e7366657257697468536d616c6c4d656d6f0d0000000300000009726563697069656e740d00000006616d6f756e7409000000046d656d6f0902000000157472616e73666572576974684c617267654d656d6f170000000300000009726563697069656e740d00000006616d6f756e7409000000046d656d6f0b020000000e64656c65676174655374616b6573180000000200000009726563697069656e740d00000006616d6f756e740902000000167265747261637444656c6567617465645374616b6573190000000200000009726563697069656e740d00000006616d6f756e7409020000001961626f72745374616b6544656c65676174696f6e4576656e741a000000010000000d7472616e73616374696f6e496413020000001561636365707444656c6567617465645374616b65731b000000020000000673656e6465720d00000006616d6f756e7409020000001572656475636544656c6567617465645374616b65731c000000020000000673656e6465720d00000006616d6f756e7409020000000f7472616e7366657242796f634f6c641d0000000300000009726563697069656e740d00000006616d6f756e74090000000673796d626f6c0b020000000c7472616e7366657242796f631e0000000300000009726563697069656e740d00000006616d6f756e74180000000673796d626f6c0b020000000c6173736f63696174654963651f0000000200000008636f6e74726163740d00000006616d6f756e7409020000000f6469736173736f6369617465496365200000000200000008636f6e74726163740d00000006616d6f756e7409030000002c6469736173736f6369617465546f6b656e73466f7252656d6f766564436f6e747261637443616c6c6261636b00000000020000000f636f6e7472616374416464726573730d00000009726563697069656e740d03000000107472616e7366657243616c6c6261636b01000000030000000d7472616e73616374696f6e4964130000000673656e6465720d00000009726563697069656e740d03000000177374616b6544656c65676174696f6e43616c6c6261636b02000000030000000d7472616e73616374696f6e4964130000000673656e6465720d00000009726563697069656e740d03000000166963654173736f63696174696f6e43616c6c6261636b03000000030000000d7472616e73616374696f6e4964130000000673656e6465720d00000008636f6e74726163740d0000", + "hex" + ) +).parseAbi(); + +type Option = K | undefined; + +export interface MpcTokenContractState { + icedStakings: Option, Option>>; + locked: boolean; + stakeDelegations: Option, Option>>; + transfers: Option, Option>>; +} + +export function newMpcTokenContractState( + icedStakings: Option, Option>>, + locked: boolean, + stakeDelegations: Option, Option>>, + transfers: Option, Option>> +): MpcTokenContractState { + return { icedStakings, locked, stakeDelegations, transfers }; +} + +function fromScValueMpcTokenContractState(structValue: ScValueStruct): MpcTokenContractState { + return { + icedStakings: structValue + .getFieldValue("icedStakings")! + .optionValue() + .valueOrUndefined( + (sc1) => + new Map( + [...sc1.mapValue().map].map(([k2, v3]) => [ + k2.optionValue().valueOrUndefined((sc4) => Hash.fromBuffer(sc4.hashValue().value)), + v3.optionValue().valueOrUndefined((sc5) => fromScValueIceStaking(sc5.structValue())), + ]) + ) + ), + locked: structValue.getFieldValue("locked")!.boolValue(), + stakeDelegations: structValue + .getFieldValue("stakeDelegations")! + .optionValue() + .valueOrUndefined( + (sc6) => + new Map( + [...sc6.mapValue().map].map(([k7, v8]) => [ + k7.optionValue().valueOrUndefined((sc9) => Hash.fromBuffer(sc9.hashValue().value)), + v8 + .optionValue() + .valueOrUndefined((sc10) => fromScValueStakeDelegation(sc10.structValue())), + ]) + ) + ), + transfers: structValue + .getFieldValue("transfers")! + .optionValue() + .valueOrUndefined( + (sc11) => + new Map( + [...sc11.mapValue().map].map(([k12, v13]) => [ + k12.optionValue().valueOrUndefined((sc14) => Hash.fromBuffer(sc14.hashValue().value)), + v13 + .optionValue() + .valueOrUndefined((sc15) => fromScValueTransferInformation(sc15.structValue())), + ]) + ) + ), + }; +} + +export function deserializeMpcTokenContractState(state: StateBytes): MpcTokenContractState { + const scValue = new StateReader(state.state, fileAbi.contract, state.avlTrees).readState(); + return fromScValueMpcTokenContractState(scValue); +} + +export interface IceStaking { + amount: BN; + contract: Option; + sender: Option; +} + +export function newIceStaking( + amount: BN, + contract: Option, + sender: Option +): IceStaking { + return { amount, contract, sender }; +} + +function fromScValueIceStaking(structValue: ScValueStruct): IceStaking { + return { + amount: structValue.getFieldValue("amount")!.asBN(), + contract: structValue + .getFieldValue("contract")! + .optionValue() + .valueOrUndefined((sc16) => BlockchainAddress.fromBuffer(sc16.addressValue().value)), + sender: structValue + .getFieldValue("sender")! + .optionValue() + .valueOrUndefined((sc17) => BlockchainAddress.fromBuffer(sc17.addressValue().value)), + }; +} + +export interface StakeDelegation { + amount: BN; + delegationType: Option; + recipient: Option; + sender: Option; +} + +export function newStakeDelegation( + amount: BN, + delegationType: Option, + recipient: Option, + sender: Option +): StakeDelegation { + return { amount, delegationType, recipient, sender }; +} + +function fromScValueStakeDelegation(structValue: ScValueStruct): StakeDelegation { + return { + amount: structValue.getFieldValue("amount")!.asBN(), + delegationType: structValue + .getFieldValue("delegationType")! + .optionValue() + .valueOrUndefined((sc18) => fromScValueDelegationType(sc18.enumValue())), + recipient: structValue + .getFieldValue("recipient")! + .optionValue() + .valueOrUndefined((sc19) => BlockchainAddress.fromBuffer(sc19.addressValue().value)), + sender: structValue + .getFieldValue("sender")! + .optionValue() + .valueOrUndefined((sc20) => BlockchainAddress.fromBuffer(sc20.addressValue().value)), + }; +} + +export type DelegationType = + | DelegationTypeDelegationType$DELEGATE_STAKES + | DelegationTypeDelegationType$RETRACT_DELEGATED_STAKES; + +export enum DelegationTypeD { + DelegationType$DELEGATE_STAKES = 0, + DelegationType$RETRACT_DELEGATED_STAKES = 1, +} + +function fromScValueDelegationType(enumValue: ScValueEnum): DelegationType { + const item = enumValue.item; + if (item.name === "DelegationType$DELEGATE_STAKES") { + return fromScValueDelegationTypeDelegationType$DELEGATE_STAKES(item); + } + if (item.name === "DelegationType$RETRACT_DELEGATED_STAKES") { + return fromScValueDelegationTypeDelegationType$RETRACT_DELEGATED_STAKES(item); + } + throw Error("Should not happen"); +} + +export interface DelegationTypeDelegationType$DELEGATE_STAKES { + discriminant: DelegationTypeD.DelegationType$DELEGATE_STAKES; +} + +export function newDelegationTypeDelegationType$DELEGATE_STAKES(): DelegationTypeDelegationType$DELEGATE_STAKES { + return { discriminant: 0 }; +} + +function fromScValueDelegationTypeDelegationType$DELEGATE_STAKES( + structValue: ScValueStruct +): DelegationTypeDelegationType$DELEGATE_STAKES { + return { + discriminant: DelegationTypeD.DelegationType$DELEGATE_STAKES, + }; +} + +export interface DelegationTypeDelegationType$RETRACT_DELEGATED_STAKES { + discriminant: DelegationTypeD.DelegationType$RETRACT_DELEGATED_STAKES; +} + +export function newDelegationTypeDelegationType$RETRACT_DELEGATED_STAKES(): DelegationTypeDelegationType$RETRACT_DELEGATED_STAKES { + return { discriminant: 1 }; +} + +function fromScValueDelegationTypeDelegationType$RETRACT_DELEGATED_STAKES( + structValue: ScValueStruct +): DelegationTypeDelegationType$RETRACT_DELEGATED_STAKES { + return { + discriminant: DelegationTypeD.DelegationType$RETRACT_DELEGATED_STAKES, + }; +} + +export interface TransferInformation { + amount: Option; + recipient: Option; + sender: Option; + symbol: Option; +} + +export function newTransferInformation( + amount: Option, + recipient: Option, + sender: Option, + symbol: Option +): TransferInformation { + return { amount, recipient, sender, symbol }; +} + +function fromScValueTransferInformation(structValue: ScValueStruct): TransferInformation { + return { + amount: structValue + .getFieldValue("amount")! + .optionValue() + .valueOrUndefined((sc21) => sc21.asBN()), + recipient: structValue + .getFieldValue("recipient")! + .optionValue() + .valueOrUndefined((sc22) => BlockchainAddress.fromBuffer(sc22.addressValue().value)), + sender: structValue + .getFieldValue("sender")! + .optionValue() + .valueOrUndefined((sc23) => BlockchainAddress.fromBuffer(sc23.addressValue().value)), + symbol: structValue + .getFieldValue("symbol")! + .optionValue() + .valueOrUndefined((sc24) => sc24.stringValue()), + }; +} + +export function create(locked: boolean): Buffer { + const fnBuilder = new FnRpcBuilder("create", fileAbi.contract); + fnBuilder.addBool(locked); + return fnBuilder.getBytes(); +} + +export function stakeTokens(amount: BN): Buffer { + const fnBuilder = new FnRpcBuilder("stakeTokens", fileAbi.contract); + fnBuilder.addI64(amount); + return fnBuilder.getBytes(); +} + +export function unstakeTokens(amount: BN): Buffer { + const fnBuilder = new FnRpcBuilder("unstakeTokens", fileAbi.contract); + fnBuilder.addI64(amount); + return fnBuilder.getBytes(); +} + +export function disassociateForRemoved(contractAddress: BlockchainAddress): Buffer { + const fnBuilder = new FnRpcBuilder("disassociateForRemoved", fileAbi.contract); + fnBuilder.addAddress(contractAddress.asBuffer()); + return fnBuilder.getBytes(); +} + +export function transfer(recipient: BlockchainAddress, amount: BN): Buffer { + const fnBuilder = new FnRpcBuilder("transfer", fileAbi.contract); + fnBuilder.addAddress(recipient.asBuffer()); + fnBuilder.addI64(amount); + return fnBuilder.getBytes(); +} + +export function abort(transactionId: Hash): Buffer { + const fnBuilder = new FnRpcBuilder("abort", fileAbi.contract); + fnBuilder.addHash(transactionId.asBuffer()); + return fnBuilder.getBytes(); +} + +export function checkPendingUnstakes(): Buffer { + const fnBuilder = new FnRpcBuilder("checkPendingUnstakes", fileAbi.contract); + return fnBuilder.getBytes(); +} + +export function checkVestedTokens(): Buffer { + const fnBuilder = new FnRpcBuilder("checkVestedTokens", fileAbi.contract); + return fnBuilder.getBytes(); +} + +export function transferWithSmallMemo(recipient: BlockchainAddress, amount: BN, memo: BN): Buffer { + const fnBuilder = new FnRpcBuilder("transferWithSmallMemo", fileAbi.contract); + fnBuilder.addAddress(recipient.asBuffer()); + fnBuilder.addI64(amount); + fnBuilder.addI64(memo); + return fnBuilder.getBytes(); +} + +export function transferWithLargeMemo( + recipient: BlockchainAddress, + amount: BN, + memo: string +): Buffer { + const fnBuilder = new FnRpcBuilder("transferWithLargeMemo", fileAbi.contract); + fnBuilder.addAddress(recipient.asBuffer()); + fnBuilder.addI64(amount); + fnBuilder.addString(memo); + return fnBuilder.getBytes(); +} + +export function delegateStakes(recipient: BlockchainAddress, amount: BN): Buffer { + const fnBuilder = new FnRpcBuilder("delegateStakes", fileAbi.contract); + fnBuilder.addAddress(recipient.asBuffer()); + fnBuilder.addI64(amount); + return fnBuilder.getBytes(); +} + +export function retractDelegatedStakes(recipient: BlockchainAddress, amount: BN): Buffer { + const fnBuilder = new FnRpcBuilder("retractDelegatedStakes", fileAbi.contract); + fnBuilder.addAddress(recipient.asBuffer()); + fnBuilder.addI64(amount); + return fnBuilder.getBytes(); +} + +export function abortStakeDelegationEvent(transactionId: Hash): Buffer { + const fnBuilder = new FnRpcBuilder("abortStakeDelegationEvent", fileAbi.contract); + fnBuilder.addHash(transactionId.asBuffer()); + return fnBuilder.getBytes(); +} + +export function acceptDelegatedStakes(sender: BlockchainAddress, amount: BN): Buffer { + const fnBuilder = new FnRpcBuilder("acceptDelegatedStakes", fileAbi.contract); + fnBuilder.addAddress(sender.asBuffer()); + fnBuilder.addI64(amount); + return fnBuilder.getBytes(); +} + +export function reduceDelegatedStakes(sender: BlockchainAddress, amount: BN): Buffer { + const fnBuilder = new FnRpcBuilder("reduceDelegatedStakes", fileAbi.contract); + fnBuilder.addAddress(sender.asBuffer()); + fnBuilder.addI64(amount); + return fnBuilder.getBytes(); +} + +export function transferByocOld(recipient: BlockchainAddress, amount: BN, symbol: string): Buffer { + const fnBuilder = new FnRpcBuilder("transferByocOld", fileAbi.contract); + fnBuilder.addAddress(recipient.asBuffer()); + fnBuilder.addI64(amount); + fnBuilder.addString(symbol); + return fnBuilder.getBytes(); +} + +export function transferByoc(recipient: BlockchainAddress, amount: BN, symbol: string): Buffer { + const fnBuilder = new FnRpcBuilder("transferByoc", fileAbi.contract); + fnBuilder.addAddress(recipient.asBuffer()); + fnBuilder.addU256(amount); + fnBuilder.addString(symbol); + return fnBuilder.getBytes(); +} + +export function associateIce(contract: BlockchainAddress, amount: BN): Buffer { + const fnBuilder = new FnRpcBuilder("associateIce", fileAbi.contract); + fnBuilder.addAddress(contract.asBuffer()); + fnBuilder.addI64(amount); + return fnBuilder.getBytes(); +} + +export function disassociateIce(contract: BlockchainAddress, amount: BN): Buffer { + const fnBuilder = new FnRpcBuilder("disassociateIce", fileAbi.contract); + fnBuilder.addAddress(contract.asBuffer()); + fnBuilder.addI64(amount); + return fnBuilder.getBytes(); +} diff --git a/src/main/abi/TokenV1.ts b/src/main/abi/TokenV1.ts new file mode 100644 index 0000000..ca8ae1f --- /dev/null +++ b/src/main/abi/TokenV1.ts @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2022 - 2023 Partisia Blockchain Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import BN from "bn.js"; +import { + AbiParser, + AbstractBuilder, + BigEndianReader, + FileAbi, + FnKinds, + FnRpcBuilder, + RpcReader, + ScValue, + ScValueEnum, + ScValueOption, + ScValueStruct, + StateReader, + TypeIndex, + StateBytes, + BlockchainAddress, +} from "@partisiablockchain/abi-client"; +import { BigEndianByteOutput } from "@secata-public/bitmanipulation-ts"; + +const fileAbi: FileAbi = new AbiParser( + Buffer.from( + "5042434142490a02000504000000000501000000085472616e736665720000000200000002746f0d00000006616d6f756e7405010000000a546f6b656e537461746500000007000000046e616d650b00000008646563696d616c73010000000673796d626f6c0b000000056f776e65720d0000000c746f74616c5f737570706c79050000000862616c616e6365730f0d0500000007616c6c6f7765640f0d0f0d0501000000134576656e74537562736372697074696f6e496400000001000000067261775f696408010000000f45787465726e616c4576656e74496400000001000000067261775f696408010000000b536563726574566172496400000001000000067261775f69640300000007010000000a696e697469616c697a65ffffffff0f00000004000000046e616d650b0000000673796d626f6c0b00000008646563696d616c73010000000c746f74616c5f737570706c790502000000087472616e73666572010000000200000002746f0d00000006616d6f756e7405020000000d62756c6b5f7472616e736665720200000001000000097472616e73666572730e0000020000000d7472616e736665725f66726f6d03000000030000000466726f6d0d00000002746f0d00000006616d6f756e7405020000001262756c6b5f7472616e736665725f66726f6d04000000020000000466726f6d0d000000097472616e73666572730e00000200000007617070726f76650500000002000000077370656e6465720d00000006616d6f756e74050200000010617070726f76655f72656c61746976650700000002000000077370656e6465720d0000000564656c74610a0001", + "hex" + ) +).parseAbi(); + +type Option = K | undefined; + +export interface Transfer { + to: BlockchainAddress; + amount: BN; +} + +export function newTransfer(to: BlockchainAddress, amount: BN): Transfer { + return { to, amount }; +} + +function fromScValueTransfer(structValue: ScValueStruct): Transfer { + return { + to: BlockchainAddress.fromBuffer(structValue.getFieldValue("to")!.addressValue().value), + amount: structValue.getFieldValue("amount")!.asBN(), + }; +} + +function buildRpcTransfer(value: Transfer, builder: AbstractBuilder) { + const structBuilder = builder.addStruct(); + structBuilder.addAddress(value.to.asBuffer()); + structBuilder.addU128(value.amount); +} + +export interface TokenState { + name: string; + decimals: number; + symbol: string; + owner: BlockchainAddress; + totalSupply: BN; + balances: Map; + allowed: Map>; +} + +export function newTokenState( + name: string, + decimals: number, + symbol: string, + owner: BlockchainAddress, + totalSupply: BN, + balances: Map, + allowed: Map> +): TokenState { + return { name, decimals, symbol, owner, totalSupply, balances, allowed }; +} + +function fromScValueTokenState(structValue: ScValueStruct): TokenState { + return { + name: structValue.getFieldValue("name")!.stringValue(), + decimals: structValue.getFieldValue("decimals")!.asNumber(), + symbol: structValue.getFieldValue("symbol")!.stringValue(), + owner: BlockchainAddress.fromBuffer(structValue.getFieldValue("owner")!.addressValue().value), + totalSupply: structValue.getFieldValue("total_supply")!.asBN(), + balances: new Map( + [...structValue.getFieldValue("balances")!.mapValue().map].map(([k1, v2]) => [ + BlockchainAddress.fromBuffer(k1.addressValue().value), + v2.asBN(), + ]) + ), + allowed: new Map( + [...structValue.getFieldValue("allowed")!.mapValue().map].map(([k3, v4]) => [ + BlockchainAddress.fromBuffer(k3.addressValue().value), + new Map( + [...v4.mapValue().map].map(([k5, v6]) => [ + BlockchainAddress.fromBuffer(k5.addressValue().value), + v6.asBN(), + ]) + ), + ]) + ), + }; +} + +export function deserializeTokenState(state: StateBytes): TokenState { + const scValue = new StateReader(state.state, fileAbi.contract, state.avlTrees).readState(); + return fromScValueTokenState(scValue); +} + +export interface EventSubscriptionId { + rawId: number; +} + +export function newEventSubscriptionId(rawId: number): EventSubscriptionId { + return { rawId }; +} + +function fromScValueEventSubscriptionId(structValue: ScValueStruct): EventSubscriptionId { + return { + rawId: structValue.getFieldValue("raw_id")!.asNumber(), + }; +} + +export interface ExternalEventId { + rawId: number; +} + +export function newExternalEventId(rawId: number): ExternalEventId { + return { rawId }; +} + +function fromScValueExternalEventId(structValue: ScValueStruct): ExternalEventId { + return { + rawId: structValue.getFieldValue("raw_id")!.asNumber(), + }; +} + +export interface SecretVarId { + rawId: number; +} + +export function newSecretVarId(rawId: number): SecretVarId { + return { rawId }; +} + +function fromScValueSecretVarId(structValue: ScValueStruct): SecretVarId { + return { + rawId: structValue.getFieldValue("raw_id")!.asNumber(), + }; +} + +export function initialize( + name: string, + symbol: string, + decimals: number, + totalSupply: BN +): Buffer { + const fnBuilder = new FnRpcBuilder("initialize", fileAbi.contract); + fnBuilder.addString(name); + fnBuilder.addString(symbol); + fnBuilder.addU8(decimals); + fnBuilder.addU128(totalSupply); + return fnBuilder.getBytes(); +} + +export function transfer(to: BlockchainAddress, amount: BN): Buffer { + const fnBuilder = new FnRpcBuilder("transfer", fileAbi.contract); + fnBuilder.addAddress(to.asBuffer()); + fnBuilder.addU128(amount); + return fnBuilder.getBytes(); +} + +export function bulkTransfer(transfers: Transfer[]): Buffer { + const fnBuilder = new FnRpcBuilder("bulk_transfer", fileAbi.contract); + const vecBuilder7 = fnBuilder.addVec(); + for (const vecEntry8 of transfers) { + buildRpcTransfer(vecEntry8, vecBuilder7); + } + return fnBuilder.getBytes(); +} + +export function transferFrom(from: BlockchainAddress, to: BlockchainAddress, amount: BN): Buffer { + const fnBuilder = new FnRpcBuilder("transfer_from", fileAbi.contract); + fnBuilder.addAddress(from.asBuffer()); + fnBuilder.addAddress(to.asBuffer()); + fnBuilder.addU128(amount); + return fnBuilder.getBytes(); +} + +export function bulkTransferFrom(from: BlockchainAddress, transfers: Transfer[]): Buffer { + const fnBuilder = new FnRpcBuilder("bulk_transfer_from", fileAbi.contract); + fnBuilder.addAddress(from.asBuffer()); + const vecBuilder9 = fnBuilder.addVec(); + for (const vecEntry10 of transfers) { + buildRpcTransfer(vecEntry10, vecBuilder9); + } + return fnBuilder.getBytes(); +} + +export function approve(spender: BlockchainAddress, amount: BN): Buffer { + const fnBuilder = new FnRpcBuilder("approve", fileAbi.contract); + fnBuilder.addAddress(spender.asBuffer()); + fnBuilder.addU128(amount); + return fnBuilder.getBytes(); +} + +export function approveRelative(spender: BlockchainAddress, delta: BN): Buffer { + const fnBuilder = new FnRpcBuilder("approve_relative", fileAbi.contract); + fnBuilder.addAddress(spender.asBuffer()); + fnBuilder.addI128(delta); + return fnBuilder.getBytes(); +} diff --git a/src/main/abi/TokenV2.ts b/src/main/abi/TokenV2.ts new file mode 100644 index 0000000..3990c43 --- /dev/null +++ b/src/main/abi/TokenV2.ts @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2022 - 2023 Partisia Blockchain Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import BN from "bn.js"; +import { + AbiParser, + AbstractBuilder, + BigEndianReader, + FileAbi, + FnKinds, + FnRpcBuilder, + RpcReader, + ScValue, + ScValueEnum, + ScValueOption, + ScValueStruct, + StateReader, + TypeIndex, + StateBytes, + BlockchainAddress, +} from "@partisiablockchain/abi-client"; +import { BigEndianByteOutput } from "@secata-public/bitmanipulation-ts"; + +const fileAbi: FileAbi = new AbiParser( + Buffer.from( + "5042434142490a020005040000000006010000000e416c6c6f7765644164647265737300000002000000056f776e65720d000000077370656e6465720d01000000085472616e736665720000000200000002746f0d00000006616d6f756e7405010000000a546f6b656e537461746500000007000000046e616d650b00000008646563696d616c73010000000673796d626f6c0b000000056f776e65720d0000000c746f74616c5f737570706c79050000000862616c616e636573190d0500000007616c6c6f7765641900000501000000134576656e74537562736372697074696f6e496400000001000000067261775f696408010000000f45787465726e616c4576656e74496400000001000000067261775f696408010000000b536563726574566172496400000001000000067261775f69640300000007010000000a696e697469616c697a65ffffffff0f00000004000000046e616d650b0000000673796d626f6c0b00000008646563696d616c73010000000c746f74616c5f737570706c790502000000087472616e73666572010000000200000002746f0d00000006616d6f756e7405020000000d62756c6b5f7472616e736665720200000001000000097472616e73666572730e0001020000000d7472616e736665725f66726f6d03000000030000000466726f6d0d00000002746f0d00000006616d6f756e7405020000001262756c6b5f7472616e736665725f66726f6d04000000020000000466726f6d0d000000097472616e73666572730e00010200000007617070726f76650500000002000000077370656e6465720d00000006616d6f756e74050200000010617070726f76655f72656c61746976650700000002000000077370656e6465720d0000000564656c74610a0002", + "hex" + ) +).parseAbi(); + +type Option = K | undefined; + +export interface AllowedAddress { + owner: BlockchainAddress; + spender: BlockchainAddress; +} + +export function newAllowedAddress( + owner: BlockchainAddress, + spender: BlockchainAddress +): AllowedAddress { + return { owner, spender }; +} + +function fromScValueAllowedAddress(structValue: ScValueStruct): AllowedAddress { + return { + owner: BlockchainAddress.fromBuffer(structValue.getFieldValue("owner")!.addressValue().value), + spender: BlockchainAddress.fromBuffer( + structValue.getFieldValue("spender")!.addressValue().value + ), + }; +} + +export interface Transfer { + to: BlockchainAddress; + amount: BN; +} + +export function newTransfer(to: BlockchainAddress, amount: BN): Transfer { + return { to, amount }; +} + +function fromScValueTransfer(structValue: ScValueStruct): Transfer { + return { + to: BlockchainAddress.fromBuffer(structValue.getFieldValue("to")!.addressValue().value), + amount: structValue.getFieldValue("amount")!.asBN(), + }; +} + +function buildRpcTransfer(value: Transfer, builder: AbstractBuilder) { + const structBuilder = builder.addStruct(); + structBuilder.addAddress(value.to.asBuffer()); + structBuilder.addU128(value.amount); +} + +export interface TokenState { + name: string; + decimals: number; + symbol: string; + owner: BlockchainAddress; + totalSupply: BN; + balances: Option>; + allowed: Option>; +} + +export function newTokenState( + name: string, + decimals: number, + symbol: string, + owner: BlockchainAddress, + totalSupply: BN, + balances: Option>, + allowed: Option> +): TokenState { + return { name, decimals, symbol, owner, totalSupply, balances, allowed }; +} + +function fromScValueTokenState(structValue: ScValueStruct): TokenState { + return { + name: structValue.getFieldValue("name")!.stringValue(), + decimals: structValue.getFieldValue("decimals")!.asNumber(), + symbol: structValue.getFieldValue("symbol")!.stringValue(), + owner: BlockchainAddress.fromBuffer(structValue.getFieldValue("owner")!.addressValue().value), + totalSupply: structValue.getFieldValue("total_supply")!.asBN(), + balances: structValue + .getFieldValue("balances")! + .avlTreeMapValue() + .mapKeysValues( + (k1) => BlockchainAddress.fromBuffer(k1.addressValue().value), + (v2) => v2.asBN() + ), + allowed: structValue + .getFieldValue("allowed")! + .avlTreeMapValue() + .mapKeysValues( + (k3) => fromScValueAllowedAddress(k3.structValue()), + (v4) => v4.asBN() + ), + }; +} + +export function deserializeTokenState(state: StateBytes): TokenState { + const scValue = new StateReader(state.state, fileAbi.contract, state.avlTrees).readState(); + return fromScValueTokenState(scValue); +} + +export interface EventSubscriptionId { + rawId: number; +} + +export function newEventSubscriptionId(rawId: number): EventSubscriptionId { + return { rawId }; +} + +function fromScValueEventSubscriptionId(structValue: ScValueStruct): EventSubscriptionId { + return { + rawId: structValue.getFieldValue("raw_id")!.asNumber(), + }; +} + +export interface ExternalEventId { + rawId: number; +} + +export function newExternalEventId(rawId: number): ExternalEventId { + return { rawId }; +} + +function fromScValueExternalEventId(structValue: ScValueStruct): ExternalEventId { + return { + rawId: structValue.getFieldValue("raw_id")!.asNumber(), + }; +} + +export interface SecretVarId { + rawId: number; +} + +export function newSecretVarId(rawId: number): SecretVarId { + return { rawId }; +} + +function fromScValueSecretVarId(structValue: ScValueStruct): SecretVarId { + return { + rawId: structValue.getFieldValue("raw_id")!.asNumber(), + }; +} + +export function initialize( + name: string, + symbol: string, + decimals: number, + totalSupply: BN +): Buffer { + const fnBuilder = new FnRpcBuilder("initialize", fileAbi.contract); + fnBuilder.addString(name); + fnBuilder.addString(symbol); + fnBuilder.addU8(decimals); + fnBuilder.addU128(totalSupply); + return fnBuilder.getBytes(); +} + +export function transfer(to: BlockchainAddress, amount: BN): Buffer { + const fnBuilder = new FnRpcBuilder("transfer", fileAbi.contract); + fnBuilder.addAddress(to.asBuffer()); + fnBuilder.addU128(amount); + return fnBuilder.getBytes(); +} + +export function bulkTransfer(transfers: Transfer[]): Buffer { + const fnBuilder = new FnRpcBuilder("bulk_transfer", fileAbi.contract); + const vecBuilder5 = fnBuilder.addVec(); + for (const vecEntry6 of transfers) { + buildRpcTransfer(vecEntry6, vecBuilder5); + } + return fnBuilder.getBytes(); +} + +export function transferFrom(from: BlockchainAddress, to: BlockchainAddress, amount: BN): Buffer { + const fnBuilder = new FnRpcBuilder("transfer_from", fileAbi.contract); + fnBuilder.addAddress(from.asBuffer()); + fnBuilder.addAddress(to.asBuffer()); + fnBuilder.addU128(amount); + return fnBuilder.getBytes(); +} + +export function bulkTransferFrom(from: BlockchainAddress, transfers: Transfer[]): Buffer { + const fnBuilder = new FnRpcBuilder("bulk_transfer_from", fileAbi.contract); + fnBuilder.addAddress(from.asBuffer()); + const vecBuilder7 = fnBuilder.addVec(); + for (const vecEntry8 of transfers) { + buildRpcTransfer(vecEntry8, vecBuilder7); + } + return fnBuilder.getBytes(); +} + +export function approve(spender: BlockchainAddress, amount: BN): Buffer { + const fnBuilder = new FnRpcBuilder("approve", fileAbi.contract); + fnBuilder.addAddress(spender.asBuffer()); + fnBuilder.addU128(amount); + return fnBuilder.getBytes(); +} + +export function approveRelative(spender: BlockchainAddress, delta: BN): Buffer { + const fnBuilder = new FnRpcBuilder("approve_relative", fileAbi.contract); + fnBuilder.addAddress(spender.asBuffer()); + fnBuilder.addI128(delta); + return fnBuilder.getBytes(); +} diff --git a/src/main/client/AccountData.ts b/src/main/client/AccountData.ts new file mode 100644 index 0000000..2211e5f --- /dev/null +++ b/src/main/client/AccountData.ts @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2022 - 2023 Partisia Blockchain Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +/** + * Interface to specify some values of a PBC account. + * The nonce is needed when building transactions. + */ +export interface AccountData { + address: string; + nonce: number; +} diff --git a/src/main/client/AvlClient.ts b/src/main/client/AvlClient.ts new file mode 100644 index 0000000..613f3b0 --- /dev/null +++ b/src/main/client/AvlClient.ts @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2022 - 2023 Partisia Blockchain Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import { getRequest } from "./BaseClient"; +import { Buffer } from "buffer"; + +export class AvlClient { + private readonly host: string; + private readonly shards: string[]; + + constructor(host: string, shards: string[]) { + this.host = host; + this.shards = shards; + } + + public getContractState(address: string): Promise { + return getRequest(this.contractStateQueryUrl(address) + "?stateOutput=binary"); + } + + public async getContractStateAvlValue( + address: string, + treeId: number, + key: Buffer + ): Promise { + const data = await getRequest<{ data: string }>( + `${this.contractStateQueryUrl(address)}/avl/${treeId}/${key.toString("hex")}` + ); + return data === undefined ? undefined : Buffer.from(data.data, "base64"); + } + + public getContractStateAvlNextN( + address: string, + treeId: number, + key: Buffer | undefined, + n: number + ): Promise> | undefined> { + if (key === undefined) { + return getRequest>>( + `${this.contractStateQueryUrl(address)}/avl/${treeId}/next?n=${n}` + ); + } else { + return getRequest>>( + `${this.contractStateQueryUrl(address)}/avl/${treeId}/next/${key.toString("hex")}?n=${n}` + ); + } + } + + private contractStateQueryUrl(address: string): string { + return `${this.host}/shards/${this.shardForAddress(address)}/blockchain/contracts/${address}`; + } + + private shardForAddress(address: string): string { + const numOfShards = this.shards.length; + const buffer = Buffer.from(address, "hex"); + const shardIndex = Math.abs(buffer.readInt32BE(17)) % numOfShards; + return this.shards[shardIndex]; + } +} diff --git a/src/main/client/BaseClient.ts b/src/main/client/BaseClient.ts new file mode 100644 index 0000000..bde3e7a --- /dev/null +++ b/src/main/client/BaseClient.ts @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2022 - 2023 Partisia Blockchain Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +// Helper functions for building and sending get requests, and receiving json responses. + +const getHeaders: HeadersInit = { + Accept: "application/json, text/plain, */*", +}; + +const postHeaders: HeadersInit = { + Accept: "application/json, text/plain, */*", + "Content-Type": "application/json", +}; + +export type RequestType = "GET" | "PUT"; + +function buildOptions(method: RequestType, headers: HeadersInit, entityBytes: T) { + const result: RequestInit = { method, headers, body: null }; + + if (entityBytes != null) { + result.body = JSON.stringify(entityBytes); + } + return result; +} + +export function getRequest(url: string): Promise { + const options = buildOptions("GET", getHeaders, null); + return handleFetch(fetch(url, options)); +} + +export function putRequest(url: string, object: T): Promise { + const options = buildOptions("PUT", postHeaders, object); + return handleFetch(fetch(url, options)); +} + +function handleFetch(promise: Promise): Promise { + return promise + .then((response) => { + if (response.status === 200) { + return response.json(); + } else { + return undefined; + } + }) + .catch(() => undefined); +} diff --git a/src/main/client/BufferWriter.ts b/src/main/client/BufferWriter.ts new file mode 100644 index 0000000..a3bd585 --- /dev/null +++ b/src/main/client/BufferWriter.ts @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2022 - 2023 Partisia Blockchain Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import BN from "bn.js"; + +/** + * Utility class used to write specific types of values to a buffer. + */ +export class BufferWriter { + private buffer: Buffer; + + constructor() { + this.buffer = Buffer.alloc(0); + } + + public readonly writeIntBE = (int: number): void => { + const buffer = Buffer.alloc(4); + buffer.writeInt32BE(int, 0); + this.appendBuffer(buffer); + }; + + public readonly writeLongBE = (long: BN): void => { + this.writeNumberBE(long, 8); + }; + + public readonly writeNumberBE = (num: BN, byteCount: number): void => { + const buffer = num.toTwos(byteCount * 8).toArrayLike(Buffer, "be", byteCount); + this.appendBuffer(buffer); + }; + + public readonly writeBuffer = (buffer: Buffer): void => { + this.appendBuffer(buffer); + }; + + public readonly writeDynamicBuffer = (buffer: Buffer): void => { + this.writeIntBE(buffer.length); + this.writeBuffer(buffer); + }; + + public readonly writeHexString = (hex: string): void => { + this.appendBuffer(Buffer.from(hex, "hex")); + }; + + public readonly toBuffer = (): Buffer => { + return this.buffer.slice(); + }; + + private readonly appendBuffer = (buffer: Buffer) => { + this.buffer = Buffer.concat([this.buffer, buffer]); + }; +} diff --git a/src/main/client/ContractData.ts b/src/main/client/ContractData.ts new file mode 100644 index 0000000..1824752 --- /dev/null +++ b/src/main/client/ContractData.ts @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2022 - 2023 Partisia Blockchain Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +/** + * Types specifying the structure of the contract data returned from the PBC client. + */ + +export type ContractType = "PUBLIC"; + +export interface ContractCore { + type: ContractType; + address: string; + jarHash: string; + storageLength: number; + abi: string; +} + +export type ContractData = ContractCore & { serializedContract: T }; diff --git a/src/main/client/CryptoUtils.ts b/src/main/client/CryptoUtils.ts new file mode 100644 index 0000000..15a86eb --- /dev/null +++ b/src/main/client/CryptoUtils.ts @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2022 - 2023 Partisia Blockchain Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import { Cipher, createCipheriv } from "crypto"; +import { ec as Elliptic } from "elliptic"; +import { sha256 } from "hash.js"; +import BN from "bn.js"; + +const ec = new Elliptic("secp256k1"); + +/** + * Generates a new key pair. + * + * @return the generated key pair. + */ +function generateKeyPair(): Elliptic.KeyPair { + return ec.genKeyPair(); +} + +/** + * Create a shared secret from a private and a public key. + * @param keyPair the private key. + * @param publicKey the public key. + * @return the shared secret. + */ +function createSharedKey(keyPair: Elliptic.KeyPair, publicKey: Buffer): Buffer { + const pairFromBuffer: Elliptic.KeyPair = ec.keyFromPublic(publicKey); + const sharedRandom: BN = keyPair.derive(pairFromBuffer.getPublic()); + + let sharedBuffer = sharedRandom.toArrayLike(Buffer, "be"); + if (sharedRandom.bitLength() % 8 === 0) { + // Ensure that a sign bit is present in the byte encoding + sharedBuffer = Buffer.concat([Buffer.alloc(1), sharedBuffer]); + } + return hashBuffer(sharedBuffer); +} + +/** + * Create an aes cipher from a private key and the public key of the receiver of the encrypted message. + * + * @param keyPair the private key. + * @param publicKey the public key of the receiver. + */ +function createAesForParty(keyPair: Elliptic.KeyPair, publicKey: Buffer): Cipher { + const sharedKey = createSharedKey(keyPair, publicKey); + const iv = sharedKey.slice(0, 16); + const secretKey = sharedKey.slice(16, 32); + return createCipheriv("aes-128-cbc", secretKey, iv); +} + +export interface Signature { + r: BN; + s: BN; + recoveryParam: number | null; +} + +/** + * Determines the recoveryParam for the given signature. + * + * @param signature Signature to determine recovery param for. + * @param msg Signed message. + * @param publicKeyBuffer Public key to act as reference + */ +function signatureFillInRecoveryId(signature: Signature, msg: Buffer, publicKeyBuffer: Buffer) { + const keyPair = ec.keyFromPublic(publicKeyBuffer); + const publicKey = keyPair.getPublic(); + + const signatureOptions = { + r: signature.r, + s: signature.s, + }; + + // NOTE: Type annotations are incorrect for below method + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signature.recoveryParam = ec.getKeyRecoveryParam(msg as any, signatureOptions, publicKey as any); +} + +/** + * Serializes a signature into byte. + * + * @param signature the signature. + * @return the bytes. + */ +function signatureToBuffer(signature: Signature): Buffer { + if (signature.recoveryParam == null) { + throw new Error("Recovery parameter is null"); + } + return Buffer.concat([ + Buffer.from([signature.recoveryParam]), + signature.r.toArrayLike(Buffer, "be", 32), + signature.s.toArrayLike(Buffer, "be", 32), + ]); +} + +/** + * Computes the account address based on a key pair. + * + * @param keyPair the keypair of the account. + * @return the address of the account. + */ +function keyPairToAccountAddress(keyPair: Elliptic.KeyPair): string { + const publicKey = keyPair.getPublic(false, "array"); + const hash = sha256(); + hash.update(publicKey); + return "00" + hash.digest("hex").substring(24); +} + +/** + * Creates a keypair based on the private key. + * + * @param privateKey the private key as a hex string. + * @return the keypair. + */ +function privateKeyToKeypair(privateKey: string): Elliptic.KeyPair { + return ec.keyFromPrivate(privateKey, "hex"); +} + +/** + * Computes the public key from a private key. + * + * @param privateKey the private key. + * @return the public key. + */ +function privateKeyToPublicKey(privateKey: string): Buffer { + const keyPair = privateKeyToKeypair(privateKey); + return Buffer.from(keyPair.getPublic(true, "array")); +} + +/** + * Computes the account address based on a private key. + * + * @param privateKey the private key. + * @return the account address. + */ +function privateKeyToAccountAddress(privateKey: string): string { + return keyPairToAccountAddress(privateKeyToKeypair(privateKey)); +} + +/** + * Computes the account address based on a public key. + * + * @param publicKey the public key. + * @return the account address. + */ +function publicKeyToAccountAddress(publicKey: Buffer): string { + return keyPairToAccountAddress(ec.keyFromPublic(publicKey)); +} + +/** + * Hashes the buffers. + * + * @param buffers the buffers to be hashed. + * @return the hash. + */ +function hashBuffers(buffers: Buffer[]): Buffer { + const hash = sha256(); + + for (const buffer of buffers) { + hash.update(buffer); + } + + return Buffer.from(hash.digest()); +} + +/** + * Hashes the buffer. + * + * @param buffer the buffer to be hashed. + * @return the hash. + */ +function hashBuffer(buffer: Buffer): Buffer { + return hashBuffers([buffer]); +} + +/** A collection of useful crypto functions. */ +export const CryptoUtils = { + generateKeyPair, + createSharedKey, + createAesForParty, + signatureToBuffer, + keyPairToAccountAddress, + privateKeyToKeypair, + privateKeyToPublicKey, + privateKeyToAccountAddress, + publicKeyToAccountAddress, + hashBuffers, + hashBuffer, + signatureFillInRecoveryId, +}; diff --git a/src/main/client/PbcClient.ts b/src/main/client/PbcClient.ts new file mode 100644 index 0000000..6f52500 --- /dev/null +++ b/src/main/client/PbcClient.ts @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2022 - 2023 Partisia Blockchain Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import { AccountData } from "./AccountData"; +import { getRequest } from "./BaseClient"; +import { ContractCore, ContractData } from "./ContractData"; +import { ExecutedTransactionDto } from "./TransactionData"; + +/** + * Web client that can get data from PBC. + */ +export class PbcClient { + readonly host: string; + + constructor(host: string) { + this.host = host; + } + + public getContractData( + address: string, + withState = true + ): Promise | undefined> { + const query = "?requireContractState=" + withState; + return getRequest(this.host + "/blockchain/contracts/" + address + query); + } + + public getAccountData(address: string): Promise { + return getRequest(this.host + "/blockchain/account/" + address).then( + (response?: AccountData) => { + if (response != null) { + response.address = address; + } + return response; + } + ); + } + + public getExecutedTransaction( + identifier: string, + requireFinal = true + ): Promise { + const query = "?requireFinal=" + requireFinal; + return getRequest(this.host + "/blockchain/transaction/" + identifier + query); + } +} diff --git a/src/main/client/ShardedClient.ts b/src/main/client/ShardedClient.ts new file mode 100644 index 0000000..9326bfc --- /dev/null +++ b/src/main/client/ShardedClient.ts @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2022 - 2023 Partisia Blockchain Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import { AccountData } from "./AccountData"; +import { putRequest } from "./BaseClient"; +import { ContractCore, ContractData } from "./ContractData"; +import { PbcClient } from "./PbcClient"; +import { + ExecutedTransactionDto, + PutTransactionWasSuccessful, + PutTransactionWasUnsuccessful, + ShardId, + TransactionPointer, +} from "./TransactionData"; + +export interface ShardSuccessfulTransactionResponse extends PutTransactionWasSuccessful { + shard: ShardId; +} + +export type ShardPutTransactionResponse = + | ShardSuccessfulTransactionResponse + | PutTransactionWasUnsuccessful; + +/** + * Web client that can handle the sending requests to the correct shard of PBC. + */ +export class ShardedClient { + private readonly masterClient: PbcClient; + private readonly shardClients: { [key: string]: PbcClient }; + private readonly shards: string[]; + private readonly baseUrl: string; + + constructor(baseUrl: string, shards: string[]) { + this.baseUrl = baseUrl; + this.shards = shards; + this.masterClient = new PbcClient(baseUrl); + this.shardClients = {}; + for (const shard of shards) { + this.shardClients[shard] = new PbcClient(baseUrl + "/shards/" + shard); + } + } + + public getClient(shardId: ShardId): PbcClient { + if (shardId == null || this.shards.length === 0) { + return this.masterClient; + } else { + return this.shardClients[shardId]; + } + } + + public shardForAddress(address: string): string | null { + if (this.shards.length === 0) { + return null; + } else { + const buffer = Buffer.from(address, "hex"); + const shardIndex = Math.abs(buffer.readInt32BE(17)) % this.shards.length; + return this.shards[shardIndex]; + } + } + + public getAccountData(address: string): Promise { + return this.clientForAddress(address).getAccountData(address); + } + + public getContractData( + address: string, + withState?: true + ): Promise | undefined>; + public getContractData( + address: string, + withState?: boolean + ): Promise | ContractCore | undefined> { + const requireState = withState === undefined || withState; + if (requireState) { + return this.clientForAddress(address).getContractData(address, requireState); + } else { + return this.clientForAddress(address).getContractData(address, requireState); + } + } + + public getExecutedTransaction( + shard: ShardId, + identifier: string, + requireFinal?: boolean + ): Promise { + return this.getClient(shard).getExecutedTransaction(identifier, requireFinal); + } + + public putTransaction(transaction: Buffer): Promise { + const byteJson = { payload: transaction.toString("base64") }; + return putRequest(this.baseUrl + "/chain/transactions", byteJson); + } + + private clientForAddress(address: string) { + return this.getClient(this.shardForAddress(address)); + } +} diff --git a/src/main/client/TransactionApi.ts b/src/main/client/TransactionApi.ts new file mode 100644 index 0000000..cff4e82 --- /dev/null +++ b/src/main/client/TransactionApi.ts @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2022 - 2023 Partisia Blockchain Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import { ConnectedWallet } from "../shared/ConnectedWallet"; +import { ShardId, PutTransactionWasSuccessful } from "./TransactionData"; +import { ShardedClient } from "./ShardedClient"; + +/** + * Error raised when a transaction failed to execute on the blockchain. + */ +export class TransactionFailedError extends Error { + public readonly putTransaction: PutTransactionWasSuccessful; + + constructor(message: string, putTransaction: PutTransactionWasSuccessful) { + super(message); + this.name = this.constructor.name; + this.putTransaction = putTransaction; + } +} + +/** + * API for sending transactions to PBC. + * The API uses a connected user wallet, to sign and send the transaction. + * If the transaction was successful it calls a provided function to update the contract state in + * the UI. + */ +export class TransactionApi { + public static readonly TRANSACTION_TTL: number = 60_000; + private static readonly DELAY_BETWEEN_RETRIES = 1_000; + private static readonly MAX_TRIES = TransactionApi.TRANSACTION_TTL / this.DELAY_BETWEEN_RETRIES; + private readonly userWallet: ConnectedWallet; + private readonly fetchUpdatedState: () => void; + private readonly client: ShardedClient; + + constructor(client: ShardedClient, userWallet: ConnectedWallet, fetch: () => void) { + this.userWallet = userWallet; + this.fetchUpdatedState = fetch; + this.client = client; + } + + public async sendTransactionAndWait( + address: string, + rpc: Buffer, + gasCost: number + ): Promise { + const putResponse = await this.userWallet.signAndSendTransaction( + this.client, + { + rpc, + address, + }, + gasCost + ); + + if (!putResponse.putSuccessful) { + throw new Error("Blockchain refused transaction. Do you have enough gas?"); + } + + await this.waitForTransaction( + putResponse.shard, + putResponse.transactionHash, + putResponse as PutTransactionWasSuccessful + ); + + this.fetchUpdatedState(); + return putResponse as PutTransactionWasSuccessful; + } + + private readonly delay = (millis: number): Promise => { + return new Promise((resolve) => setTimeout(resolve, millis)); + }; + + private readonly waitForTransaction = ( + shard: ShardId, + identifier: string, + originalTransaction: PutTransactionWasSuccessful, + tryCount = 0 + ): Promise => { + return this.client.getExecutedTransaction(shard, identifier).then((executedTransaction) => { + if (executedTransaction == null) { + if (tryCount >= TransactionApi.MAX_TRIES) { + throw new TransactionFailedError( + 'Transaction "' + identifier + '" not finalized at shard "' + shard + '"', + originalTransaction + ); + } else { + return this.delay(TransactionApi.DELAY_BETWEEN_RETRIES).then(() => + this.waitForTransaction(shard, identifier, originalTransaction, tryCount + 1) + ); + } + } else if (!executedTransaction.executionSucceeded) { + throw new TransactionFailedError( + 'Transaction "' + identifier + '" failed at shard "' + shard + '"', + originalTransaction + ); + } else { + return Promise.all( + executedTransaction.events.map((e) => + this.waitForTransaction(e.destinationShard, e.identifier, originalTransaction) + ) + ).then(() => undefined); + } + }); + }; +} diff --git a/src/main/client/TransactionData.ts b/src/main/client/TransactionData.ts new file mode 100644 index 0000000..7648580 --- /dev/null +++ b/src/main/client/TransactionData.ts @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2022 - 2023 Partisia Blockchain Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import { Buffer } from "buffer"; + +/** + * This file specifies the data format for transactions, executed transactions and events. + */ +export interface TransactionInner { + nonce: number; + validTo: string; + cost: string; +} + +interface ExecutedTransactionDtoInner { + transactionPayload: string; + block: string; + blockTime: number; + productionTime: number; + identifier: string; + executionSucceeded: boolean; + failureCause?: FailureCause; + events: EventData[]; + finalized: boolean; +} + +export type ExecutedEventTransactionDto = ExecutedTransactionDtoInner; + +export interface ExecutedSignedTransactionDto extends ExecutedTransactionDtoInner { + from: string; + interactionJarHash: string; +} + +export type ExecutedTransactionDto = ExecutedEventTransactionDto | ExecutedSignedTransactionDto; + +export type TransactionPayload = InteractWithContract & PayloadT; + +export interface InteractWithContract { + address: string; +} + +export interface Rpc { + rpc: Buffer; +} + +export interface PutTransactionWasSuccessful { + putSuccessful: true; + transactionHash: string; +} + +export interface PutTransactionWasUnsuccessful { + putSuccessful: false; +} + +export type ShardId = string | null; + +export interface EventData { + identifier: string; + destinationShard: ShardId; +} + +export interface FailureCause { + errorMessage: string; + stackTrace: string; +} + +export interface TransactionPointer { + identifier: string; + destinationShardId: string; +} diff --git a/src/main/client/TransactionSerialization.ts b/src/main/client/TransactionSerialization.ts new file mode 100644 index 0000000..db01b0b --- /dev/null +++ b/src/main/client/TransactionSerialization.ts @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2022 - 2023 Partisia Blockchain Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import BN from "bn.js"; +import { BufferWriter } from "./BufferWriter"; +import { Rpc, TransactionInner, TransactionPayload } from "./TransactionData"; + +/** + * Helper function to serialize a transaction into bytes. + * @param inner the inner transaction + * @param data the actual payload + */ +export function serializeTransaction( + inner: TransactionInner, + data: TransactionPayload +): Buffer { + const bufferWriter = new BufferWriter(); + serializeTransactionInner(bufferWriter, inner); + bufferWriter.writeHexString(data.address); + bufferWriter.writeDynamicBuffer(data.rpc); + return bufferWriter.toBuffer(); +} + +function serializeTransactionInner(bufferWriter: BufferWriter, inner: TransactionInner) { + bufferWriter.writeLongBE(new BN(inner.nonce)); + bufferWriter.writeLongBE(new BN(inner.validTo)); + bufferWriter.writeLongBE(new BN(inner.cost)); +} diff --git a/src/main/constant.ts b/src/main/constant.ts new file mode 100644 index 0000000..996604c --- /dev/null +++ b/src/main/constant.ts @@ -0,0 +1,4 @@ +export const NETWORK_ID: string = "Partisia Blockchain"; +export const NETWORK_SHARDS: string[] = [ "Shard0", "Shard1", "Shard2" ]; +export const NODE_BASE_URL: string = "https://reader.partisiablockchain.com"; +export const BROWSER_BASE_URL: string = "https://browser.partisiablockchain.com"; diff --git a/src/main/index.html b/src/main/index.html new file mode 100644 index 0000000..9c7af7a --- /dev/null +++ b/src/main/index.html @@ -0,0 +1,48 @@ + + + + PBC wallet integration + + + + + + + + + + Example Clients + MPC20-V1 Token Contract + MPC20-V2 Token Contract + MPC Token + + + + diff --git a/src/main/pbc-ledger-client/PbcLedgerClient.ts b/src/main/pbc-ledger-client/PbcLedgerClient.ts new file mode 100644 index 0000000..b6753b8 --- /dev/null +++ b/src/main/pbc-ledger-client/PbcLedgerClient.ts @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2022 - 2023 Partisia Blockchain Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import BIPPath from "bip32-path"; +import type Transport from "@ledgerhq/hw-transport"; +import BN from "bn.js"; +import { Signature } from "../client/CryptoUtils"; + +/** + * Serializes a BIP-32 path to a buffer. + */ +export function bip32Buffer(path: string): Buffer { + // Bip format to numbers + + const pathElements: number[] = BIPPath.fromString(path).toPathArray(); + const buffer = Buffer.alloc(1 + pathElements.length * 4); + buffer[0] = pathElements.length; + pathElements.forEach((pathElement, pathIdx) => { + buffer.writeUInt32BE(pathElement, 1 + 4 * pathIdx); + }); + return buffer; +} + +/** + * Serializes a number as an unsigned 32-bit integer as a buffer. + */ +function uint32BeBuffer(v: number): Buffer { + const buffer = Buffer.alloc(4); + buffer.writeUInt32BE(v, 0); + return buffer; +} + +/** + * The maximum length of each APDU packets to send to the Ledger device. + * + * @see https://en.wikipedia.org/wiki/Smart_card_application_protocol_data_unit + */ +const MAX_APDU_DATA_LENGTH = 255; + +/** + * Instruction class for the PBC App. + * + * @see https://en.wikipedia.org/wiki/Smart_card_application_protocol_data_unit + */ +const CLA = 0xe0; + +/** + * Instructions for the PBC App. + * + * @see https://en.wikipedia.org/wiki/Smart_card_application_protocol_data_unit + */ +enum INS { + GET_VERSION = 0x03, + GET_APP_NAME = 0x04, + SIGN_TRANSACTION = 0x06, + GET_ADDRESS = 0x07, +} + +/** + * First parameters for the PBC App. + * + * @see https://en.wikipedia.org/wiki/Smart_card_application_protocol_data_unit + */ +enum P1 { + P1_FIRST_CHUNK = 0x00, + P1_NOT_FIRST_CHUNK = 0x01, +} + +const P1_CONFIRM_ADDRESS_ON_SCREEN = 0x01; + +/** + * Second parameters for the PBC App. + * + * @see https://en.wikipedia.org/wiki/Smart_card_application_protocol_data_unit + */ +enum P2 { + P2_LAST_CHUNK = 0x00, + P2_NOT_LAST_CHUNK = 0x80, +} + +/** + * Splits the given buffer into chunks of at most chunkSize. + */ +function chunkifyBuffer(buffer: Buffer, chunkSize: number): Buffer[] { + const chunks: Buffer[] = []; + for (let i = 0; i < buffer.length; i += chunkSize) { + chunks.push(buffer.slice(i, Math.min(buffer.length, i + chunkSize))); + } + return chunks; +} + +/** + * Wrapper class capable of interacting with the Ledger hardware wallet through + * APDU calls. + */ +export class PbcLedgerClient { + private readonly ledgerTransport: Transport; + + constructor(ledgerTransport: Transport) { + this.ledgerTransport = ledgerTransport; + } + + /** + * Asks the Ledger hardware wallet about the address that it will sign for. + */ + public getAddress(keyPath: string, confirmOnScreen = false): Promise { + return this.ledgerTransport + .send( + CLA, + INS.GET_ADDRESS, + confirmOnScreen ? P1_CONFIRM_ADDRESS_ON_SCREEN : P1.P1_FIRST_CHUNK, + P2.P2_LAST_CHUNK, + bip32Buffer(keyPath) + ) + .then((result) => result.slice(0, 21).toString("hex")); + } + + /** + * Attempts to sign a transaction by communicating with a Ledger hardware + * wallet. + * + * Returns a signature as a promised buffer. + */ + public async signTransaction( + keyPath: string, + serializedTransaction: Buffer, + chainId: string + ): Promise { + const chainIdBuffer = Buffer.from(chainId, "utf8"); + + // Setup data to send + const initialChunkData = Buffer.concat([ + bip32Buffer(keyPath), + uint32BeBuffer(chainIdBuffer.length), + chainIdBuffer, + ]); + + const subsequentChunkData = chunkifyBuffer(serializedTransaction, MAX_APDU_DATA_LENGTH); + + // Setup promise flow + let result = await this.ledgerTransport.send( + CLA, + INS.SIGN_TRANSACTION, + P1.P1_FIRST_CHUNK, + P2.P2_NOT_LAST_CHUNK, + initialChunkData + ); + + // Iterate blocks + for (let chunkIdx = 0; chunkIdx < subsequentChunkData.length; chunkIdx++) { + const chunk = subsequentChunkData[chunkIdx]; + const isLastChunk = chunkIdx == subsequentChunkData.length - 1; + result = await this.ledgerTransport.send( + CLA, + INS.SIGN_TRANSACTION, + P1.P1_NOT_FIRST_CHUNK, + isLastChunk ? P2.P2_LAST_CHUNK : P2.P2_NOT_LAST_CHUNK, + chunk + ); + } + + // Deserialize signature from the transfer format + return { + recoveryParam: result[0], + r: new BN(result.slice(1, 32 + 1)), + s: new BN(result.slice(32 + 1, 32 + 32 + 1)), + }; + } +} diff --git a/src/main/pbc-ledger-client/bip32-path.d.ts b/src/main/pbc-ledger-client/bip32-path.d.ts new file mode 100644 index 0000000..aa985a9 --- /dev/null +++ b/src/main/pbc-ledger-client/bip32-path.d.ts @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2022 - 2023 Partisia Blockchain Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +declare module "bip32-path"; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +class BIPPath {} diff --git a/src/main/shared/ConnectedWallet.ts b/src/main/shared/ConnectedWallet.ts new file mode 100644 index 0000000..6b009ef --- /dev/null +++ b/src/main/shared/ConnectedWallet.ts @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2022 - 2023 Partisia Blockchain Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import { ShardPutTransactionResponse } from "../client/ShardedClient"; +import { Rpc, TransactionPayload } from "../client/TransactionData"; +import { ShardedClient } from "../client/ShardedClient"; + +/** + * Unified interface for connected MPC wallets. + * + * These wallets are capable of reporting their address and can sign and send + * a transaction. + */ +export interface ConnectedWallet { + /** + * The address that transactions will be sent from. + */ + readonly address: string; + /** + * Method to sign and send a transaction to the blockchain. + */ + readonly signAndSendTransaction: ( + client: ShardedClient, + payload: TransactionPayload, + cost?: string | number + ) => Promise; +} diff --git a/src/main/shared/LedgerIntegration.ts b/src/main/shared/LedgerIntegration.ts new file mode 100644 index 0000000..1074d6e --- /dev/null +++ b/src/main/shared/LedgerIntegration.ts @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2022 - 2023 Partisia Blockchain Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import { ConnectedWallet } from "./ConnectedWallet"; +import { ShardedClient } from "../client/ShardedClient"; +import { ShardPutTransactionResponse } from "../client/ShardedClient"; +import { TransactionApi } from "../client/TransactionApi"; +import { PbcLedgerClient } from "../pbc-ledger-client/PbcLedgerClient"; +import { listen } from "@ledgerhq/logs"; +import { CryptoUtils } from "../client/CryptoUtils"; +import { Rpc, TransactionPayload } from "../client/TransactionData"; +import { serializeTransaction } from "../client/TransactionSerialization"; +import { NETWORK_ID } from "../constant"; + +import TransportWebUSB from "@ledgerhq/hw-transport-webusb"; +//import TransportWebHID from "@ledgerhq/hw-transport-webhid"; + +const DEFAULT_KEYPATH = "44'/3757'/0'/0/0"; + +const HACKY_DOWNLOAD_SIGNED_TRANSACTION: boolean = true; + +/** + * Internal utility for signing a transaction using a Ledger hardware device, + * and then sending the transaction. + * + * @param client Blockchain client. + * @param payload Payload to sign and send. + * @param cost Cost to use to send. + * @param ledgerClient Client for using the PBC App on the Ledger Hardware + * Device. + * @param senderAddress Address of the sender + */ +async function signAndSendTransaction( + client: ShardedClient, + payload: TransactionPayload, + cost: string | number, + ledgerClient: PbcLedgerClient, + senderAddress: string +): Promise { + + // To send a transaction we need some up-to-date account information, i.e. the + // current account nonce. + const accountData = await client.getAccountData(senderAddress); + + if (accountData == null) { + throw new Error("Account data was null"); + } + + // Account data was fetched, build and serialize the transaction + // data. + const serializedTx = serializeTransaction( + { + cost: String(cost), + nonce: accountData.nonce, + validTo: String(new Date().getTime() + TransactionApi.TRANSACTION_TTL), + }, + payload + ); + + // Use ledger device to sign transaction + const signature = await ledgerClient.signTransaction(DEFAULT_KEYPATH, serializedTx, NETWORK_ID); + + const signatureBuffer = CryptoUtils.signatureToBuffer(signature); + + // Serialize transaction for sending + const transactionPayload = Buffer.concat([signatureBuffer, serializedTx]); + + if (HACKY_DOWNLOAD_SIGNED_TRANSACTION) { + const blob = new Blob([transactionPayload], {type:"application/octet-stream"}); + const url = window.URL.createObjectURL(blob); + window.open(url); + + return { putSuccessful: false }; + } + + // Send the transaction to the blockchain + const txPointer = await client.putTransaction(transactionPayload); + + // Create result + if (txPointer != null) { + return { + putSuccessful: true, + shard: txPointer.destinationShardId, + transactionHash: txPointer.identifier, + }; + } else { + return { putSuccessful: false }; + } +} + +/** + * Initializes a ConnectedWallet by connecting to a Ledger Hardware Wallet. + * + * Both the initial connection and sending of a transaction will require the + * user to interact with their Ledger device. + * + * Does not take any arguments as everything is automatically determined from the + * environment. + */ +export const connectLedger = async (): Promise => { + const ledgerTransport = await TransportWebUSB.create(); + //const ledgerTransport = await TransportWebHID.create(); + + //listen to the events which are sent by the Ledger packages in order to debug the app + // eslint-disable-next-line no-console + listen((log) => console.log(log)); + + const ledgerClient = new PbcLedgerClient(ledgerTransport); + const senderAddress = await ledgerClient.getAddress(DEFAULT_KEYPATH); + + return { + address: senderAddress, + signAndSendTransaction: (client: ShardedClient, payload: TransactionPayload, cost = 0) => + signAndSendTransaction(client, payload, cost, ledgerClient, senderAddress), + }; +}; + +/** + * Shows the address on the Ledger. + * + * Does not take any arguments as everything is automatically determined from the + * environment. + */ +export const validateLedgerConnection = async (): Promise => { + const ledgerTransport = await TransportWebUSB.create(); + //const ledgerTransport = await TransportWebHID.create(); + + const ledgerClient = new PbcLedgerClient(ledgerTransport); + await ledgerClient.getAddress(DEFAULT_KEYPATH, true); +}; diff --git a/src/main/shared/MetaMaskIntegration.ts b/src/main/shared/MetaMaskIntegration.ts new file mode 100644 index 0000000..331b496 --- /dev/null +++ b/src/main/shared/MetaMaskIntegration.ts @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2022 - 2023 Partisia Blockchain Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import { ConnectedWallet } from "./ConnectedWallet"; +import { serializeTransaction } from "../client/TransactionSerialization"; +import { TransactionApi } from "../client/TransactionApi"; +import { ShardedClient } from "../client/ShardedClient"; +import { NETWORK_ID } from "../constant"; + +interface MetamaskRequestArguments { + /** The RPC method to request. */ + method: string; + /** The params of the RPC method, if any. */ + params?: unknown[] | Record; +} + +interface MetaMask { + request(args: MetamaskRequestArguments): Promise; +} + +/** + * Initializes a ConnectedWallet by connecting to MetaMask snap. + * + * Does not take any arguments as everything is automatically determined from the + * environment. An error is thrown if the MetaMask extension is not installed. + */ +export const connectMetaMask = async (): Promise => { + const snapId = "npm:@partisiablockchain/snap"; + + if ("ethereum" in window) { + const metamask = window.ethereum as MetaMask; + + // Request snap to be installed and connected + await metamask.request({ + method: "wallet_requestSnaps", + params: { + [snapId]: {}, + }, + }); + + // Get the address of the user from the snap + const userAddress: string = await metamask.request({ + method: "wallet_invokeSnap", + params: { snapId, request: { method: "get_address" } }, + }); + + return { + address: userAddress, + signAndSendTransaction: async (client: ShardedClient, payload, cost = 0) => { + // To send a transaction we need some up-to-date account information, i.e. the + // current account nonce. + const accountData = await client.getAccountData(userAddress); + if (accountData == null) { + throw new Error("Account data was null"); + } + // Account data was fetched, build and serialize the transaction + // data. + const serializedTx = serializeTransaction( + { + cost: String(cost), + nonce: accountData.nonce, + validTo: String(new Date().getTime() + TransactionApi.TRANSACTION_TTL), + }, + payload + ); + + // Request signature from MetaMask + const signature: string = await metamask.request({ + method: "wallet_invokeSnap", + params: { + snapId: "npm:@partisiablockchain/snap", + request: { + method: "sign_transaction", + params: { + payload: serializedTx.toString("hex"), + chainId: NETWORK_ID, + }, + }, + }, + }); + + // Serialize transaction for sending + const transactionPayload = Buffer.concat([Buffer.from(signature, "hex"), serializedTx]); + + // Send the transaction to the blockchain + return client.putTransaction(transactionPayload).then((txPointer) => { + if (txPointer != null) { + return { + putSuccessful: true, + shard: txPointer.destinationShardId, + transactionHash: txPointer.identifier, + }; + } else { + return { putSuccessful: false }; + } + }); + }, + }; + } else { + throw new Error("Unable to find MetaMask extension"); + } +}; diff --git a/src/main/shared/MpcWalletIntegration.ts b/src/main/shared/MpcWalletIntegration.ts new file mode 100644 index 0000000..9715a52 --- /dev/null +++ b/src/main/shared/MpcWalletIntegration.ts @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2022 - 2023 Partisia Blockchain Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import { ConnectedWallet } from "./ConnectedWallet"; +import { serializeTransaction } from "../client/TransactionSerialization"; +import { TransactionApi } from "../client/TransactionApi"; +import { ShardedClient } from "../client/ShardedClient"; +import PartisiaSdk from "partisia-blockchain-applications-sdk"; +import { NETWORK_ID } from "../constant"; + +/** + * Initializes a new ConnectedWallet by connecting to Partisia Blockchain + * Applications MPC wallet. + * + * Does not take any arguments as everything is automatically determined from the + * environment. An error is thrown if the MPC Wallet extension is not installed. + */ +export const connectMpcWallet = async (): Promise => { + const partisiaSdk = new PartisiaSdk(); + return partisiaSdk + .connect({ + // eslint-disable-next-line + permissions: ["sign" as any], + dappName: "Wallet integration demo", + chainId: NETWORK_ID, + }) + .then(() => { + const connection = partisiaSdk.connection; + if (connection != null) { + // User connection was successful. Use the connection to build up a connected wallet + // in state. + const userAccount: ConnectedWallet = { + address: connection.account.address, + signAndSendTransaction: (client: ShardedClient, payload, cost = 0) => { + // To send a transaction we need some up-to-date account information, i.e. the + // current account nonce. + return client.getAccountData(connection.account.address).then((accountData) => { + if (accountData == null) { + throw new Error("Account data was null"); + } + // Account data was fetched, build and serialize the transaction + // data. + const serializedTx = serializeTransaction( + { + cost: String(cost), + nonce: accountData.nonce, + validTo: String(new Date().getTime() + TransactionApi.TRANSACTION_TTL), + }, + payload + ); + // Ask the MPC wallet to sign and send the transaction. + return partisiaSdk + .signMessage({ + payload: serializedTx.toString("hex"), + payloadType: "hex", + dontBroadcast: false, + }) + .then((value) => { + return { + putSuccessful: true, + shard: client.shardForAddress(connection.account.address), + transactionHash: value.trxHash, + }; + }) + .catch(() => ({ + putSuccessful: false, + })); + }); + }, + }; + return userAccount; + } else { + throw new Error("Unable to establish connection to MPC wallet"); + } + }) + .catch((error) => { + // Something went wrong with the connection. + if (error instanceof Error) { + if (error.message === "Extension not Found") { + throw new Error("Partisia Wallet Extension not found."); + } else if (error.message === "user closed confirm window") { + throw new Error("Sign in using MPC wallet was cancelled"); + } else if (error.message === "user rejected") { + throw new Error("Sign in using MPC wallet was rejected"); + } else { + throw error; + } + } else { + throw new Error(error); + } + }); +}; diff --git a/src/main/shared/PrivateKeyIntegration.ts b/src/main/shared/PrivateKeyIntegration.ts new file mode 100644 index 0000000..0c19c7a --- /dev/null +++ b/src/main/shared/PrivateKeyIntegration.ts @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2022 - 2023 Partisia Blockchain Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import { ConnectedWallet } from "./ConnectedWallet"; +import { serializeTransaction } from "../client/TransactionSerialization"; +import { TransactionApi } from "../client/TransactionApi"; +import { ShardedClient } from "../client/ShardedClient"; +import { Rpc, TransactionPayload } from "../client/TransactionData"; +import { BigEndianByteOutput } from "@secata-public/bitmanipulation-ts"; +import { CryptoUtils } from "../client/CryptoUtils"; +import { ec } from "elliptic"; +import { NETWORK_ID } from "../constant"; + +/** + * Initializes a ConnectedWallet by inputting the private key directly. + */ +export const connectPrivateKey = async ( + sender: string, + keyPair: ec.KeyPair +): Promise => { + return { + address: sender, + signAndSendTransaction: (client: ShardedClient, payload: TransactionPayload, cost = 0) => { + // To send a transaction we need some up-to-date account information, i.e. the + // current account nonce. + return client.getAccountData(sender).then((accountData) => { + if (accountData == null) { + throw new Error("Account data was null"); + } + // Account data was fetched, build and serialize the transaction + // data. + const serializedTx = serializeTransaction( + { + cost: String(cost), + nonce: accountData.nonce, + validTo: String(new Date().getTime() + TransactionApi.TRANSACTION_TTL), + }, + payload + ); + const hash = CryptoUtils.hashBuffers([ + serializedTx, + BigEndianByteOutput.serialize((out) => out.writeString(NETWORK_ID)), + ]); + const signature = keyPair.sign(hash); + const signatureBuffer = CryptoUtils.signatureToBuffer(signature); + + // Serialize transaction for sending + const transactionPayload = Buffer.concat([signatureBuffer, serializedTx]); + + // Send the transaction to the blockchain + return client.putTransaction(transactionPayload).then((txPointer) => { + if (txPointer != null) { + return { + putSuccessful: true, + shard: txPointer.destinationShardId, + transactionHash: txPointer.identifier, + }; + } else { + return { putSuccessful: false }; + } + }); + }); + }, + }; +}; diff --git a/src/main/shared/TokenContract.ts b/src/main/shared/TokenContract.ts new file mode 100644 index 0000000..66e2b21 --- /dev/null +++ b/src/main/shared/TokenContract.ts @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2022 - 2023 Partisia Blockchain Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import BN from "bn.js"; +import { BlockchainAddress } from "@partisiablockchain/abi-client"; +import { PutTransactionWasSuccessful } from "../client/TransactionData"; + +export interface TokenContractBasicState { + name: string; + decimals: number; + symbol: string; + owner: BlockchainAddress; + totalSupply: BN; +} + +export interface TokenBalancesResult { + balances: Map; + next_cursor: string | undefined; +} + +/** + * Unified API for token contracts. + * + * This minimal implementation only allows for transferring tokens to a single address. + * + * The implementation uses the TransactionApi to send transactions, and ABI for the contract to be + * able to build the RPC for the transfer transaction. + */ +export interface TokenContract { + /** + * Build transfer transaction RPC. + * + * @param to receiver of tokens + * @param amount number of tokens to send + * @param Large memo + */ + readonly transfer: ( + contractAddress: BlockchainAddress, + to: BlockchainAddress, + amount: BN, + memo: string, + ) => Promise; + + /** + * Determines the basic state of the contract. + */ + readonly basicState?: (contractAddress: BlockchainAddress) => Promise; + + /** + * Fetch a specific balance + */ + readonly tokenBalance?: ( + contractAddress: BlockchainAddress, + owner: BlockchainAddress + ) => Promise; + + /** + * Iterator for fetching token balances. + */ + readonly tokenBalances?: ( + contractAddress: BlockchainAddress, + cursor: string | undefined + ) => Promise; +} diff --git a/src/main/token/AppState.ts b/src/main/token/AppState.ts new file mode 100644 index 0000000..1698d51 --- /dev/null +++ b/src/main/token/AppState.ts @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2022 - 2023 Partisia Blockchain Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import { ConnectedWallet } from "../shared/ConnectedWallet"; +import { ContractAbi, BlockchainAddress } from "@partisiablockchain/abi-client"; +import { ShardedClient } from "../client/ShardedClient"; +import { TokenContract } from "../shared/TokenContract"; +import { TransactionApi } from "../client/TransactionApi"; +import { updateContractState } from "./WalletIntegration"; +import { NODE_BASE_URL, NETWORK_SHARDS } from "../constant"; + +export const CLIENT = new ShardedClient(NODE_BASE_URL, NETWORK_SHARDS); + +type TokenContractCreator = ( + client: ShardedClient, + transactionApi: TransactionApi | undefined +) => TokenContract; + +let lastBalanceKey: string | undefined; +let contractAddress: BlockchainAddress | undefined; +let currentAccount: ConnectedWallet | undefined; +let contractAbi: ContractAbi | undefined; +let tokenApi: TokenContract | undefined; + +/*eslint @typescript-eslint/no-unused-vars: ["error", { "argsIgnorePattern": "^unused" }]*/ + +let tokenContractCreator: TokenContractCreator = (unusedClient, unusedContract) => { + throw new Error("tokenContractCreator was not set"); +}; + +export function setTokenContractType(creator: TokenContractCreator) { + tokenContractCreator = creator; +} + +export const setAccount = (account: ConnectedWallet | undefined) => { + currentAccount = account; + setTokenApi(); +}; + +export const resetAccount = () => { + currentAccount = undefined; +}; + +export const isConnected = () => { + return currentAccount != null; +}; + +export const setContractAbi = (abi: ContractAbi) => { + contractAbi = abi; + setTokenApi(); +}; + +export const getContractAbi = () => { + return contractAbi; +}; + +const setTokenApi = () => { + let transactionApi = undefined; + if (currentAccount != undefined) { + transactionApi = new TransactionApi(CLIENT, currentAccount, updateContractState); + } + tokenApi = tokenContractCreator(CLIENT, transactionApi); +}; + +export const getTokenApi = () => { + return tokenApi; +}; + +export const getContractAddress = () => { + return contractAddress; +}; + +export const setContractAddress = (address: BlockchainAddress) => { + contractAddress = address; + setTokenApi(); +}; + +export const getContractLastBalanceKey = () => { + return lastBalanceKey; +}; + +export const setContractLastBalanceKey = (address: string | undefined) => { + lastBalanceKey = address; +}; diff --git a/src/main/token/Main.ts b/src/main/token/Main.ts new file mode 100644 index 0000000..5fb60d1 --- /dev/null +++ b/src/main/token/Main.ts @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2022 - 2023 Partisia Blockchain Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import { + setTokenContractType, + getTokenApi, + getContractAddress, + isConnected, + setContractAddress, +} from "./AppState"; +import { + connectMetaMaskWalletClick, + connectMpcWalletClick, + connectLedgerWalletClick, + connectPrivateKeyWalletClick, + validateLedgerConnectionClick, + disconnectWalletClick, + fetchAndDisplayMoreBalances, + updateContractState, + updateInteractionVisibility, +} from "./WalletIntegration"; +import { TokenV1Contract } from "./contract/TokenV1Contract"; +import { TokenV2Contract } from "./contract/TokenV2Contract"; +import { MpcTokenContract, MPC_TOKEN_CONTRACT_ADDRESS } from "./contract/MpcTokenContract"; +import BN from "bn.js"; +import { BlockchainAddress } from "@partisiablockchain/abi-client"; +import { TransactionFailedError } from "../client/TransactionApi"; +import { PutTransactionWasSuccessful } from "../client/TransactionData"; +import { BROWSER_BASE_URL } from "../constant"; + +// Setup event listener to connect to the MPC wallet browser extension +const connectWallet = document.querySelector("#wallet-connect-btn"); +connectWallet.addEventListener("click", connectMpcWalletClick); + +// Setup event listener to connect to the MetaMask snap +const metaMaskConnect = document.querySelector("#metamask-connect-btn"); +metaMaskConnect.addEventListener("click", connectMetaMaskWalletClick); + +// Setup event listener to connect to the ledger snap +const ledgerConnect = document.querySelector("#ledger-connect-btn"); +ledgerConnect.addEventListener("click", connectLedgerWalletClick); + +const ledgerConnectValidate = document.querySelector("#connection-link-ledger-validate"); +ledgerConnectValidate.addEventListener("click", validateLedgerConnectionClick); + +// Setup event listener to login using private key +const pkConnect = document.querySelector("#private-key-connect-btn"); +pkConnect.addEventListener("click", connectPrivateKeyWalletClick); + +// Setup event listener to drop the connection again +const disconnectWallet = document.querySelector("#wallet-disconnect-btn"); +disconnectWallet.addEventListener("click", disconnectWalletClick); + +// Setup event listener that sends a transfer transaction to the contract. +// This requires that a wallet has been connected. + +const transferBtn = document.querySelector("#transfer-btn"); +transferBtn.addEventListener("click", transferAction); + +const addressBtn = document.querySelector("#address-btn"); +addressBtn.addEventListener("click", contractAddressClick); + +const updateStateBtn = document.querySelector("#update-state-btn"); +updateStateBtn.addEventListener("click", updateContractState); + +const getBalanceBtn = document.querySelector("#get-balance-btn"); +getBalanceBtn.addEventListener("click", getBalance); + +const loadMoreBtn = document.querySelector("#balances-load-more-btn"); +loadMoreBtn.addEventListener("click", fetchAndDisplayMoreBalances); + +function setModeText(modeText: string) { + let items = document.querySelectorAll("title"); + items.forEach((item) => { + item.innerText = modeText; + }); + items = document.querySelectorAll(".mode"); + items.forEach((item) => { + item.innerText = modeText; + }); +} + +function setTokenContractByGetMode() { + const mode = window.location.search.substr(1); + if (mode == "mpc20-v1") { + setTokenContractType((client, transactionApi) => new TokenV1Contract(client, transactionApi)); + setModeText("MPC20-V1"); + + const getBalanceForm = document.querySelector("#get-balance-form"); + + getBalanceForm.classList.add("hidden"); + } else if (mode == "mpc20-v2") { + setTokenContractType((client, transactionApi) => new TokenV2Contract(client, transactionApi)); + setModeText("MPC20-V2"); + } else if (mode == "mpc-token") { + setTokenContractType((client, transactionApi) => new MpcTokenContract(client, transactionApi)); + setContractAddressUI(MPC_TOKEN_CONTRACT_ADDRESS); + setModeText("MPC Token"); + } +} + +// Setup token contract type +setTokenContractByGetMode(); + +function setContractAddressUI(address: BlockchainAddress) { + const currentAddress = document.querySelector("#current-address"); + const inputAddress = document.querySelector("#address-value"); + + currentAddress.innerText = address.asString(); + currentAddress.href = `${BROWSER_BASE_URL}/contracts/${address.asString()}`; + inputAddress.value = address.asString(); + setContractAddress(address); + updateInteractionVisibility(); + updateContractState(); +} + +/** Function for the contract address form. + * This is called when the user clicks on the connect to contract button. + * It validates the address, and then gets the state for the contract. + */ +function contractAddressClick() { + const address = (document.querySelector("#address-value")).value; + const regex = /[0-9A-Fa-f]{42}/g; + if (address === undefined) { + throw new Error("Need to provide a contract address"); + } else if (address.length != 42 || address.match(regex) == null) { + // Validate that address is 21 bytes in hexidecimal format + throw new Error(`${address} is not a valid PBC address`); + } + + setContractAddressUI(BlockchainAddress.fromString(address)); +} + +const transactionErrorMessage = document.querySelector("#sign-transaction-error"); +const transactionLinkElement = document.querySelector("#sign-transaction-link"); + +function setTransactionLink(transaction: PutTransactionWasSuccessful) { + const transactionLinkElement = document.querySelector("#sign-transaction-link"); + transactionLinkElement.innerHTML = `Transaction link in browser`; + transactionErrorMessage.innerText = ""; +} + +/** Action for the sign petition button */ +function transferAction() { + const api = getTokenApi(); + const contractAddress = getContractAddress(); + if (isConnected() && api !== undefined && contractAddress !== undefined) { + const to = document.querySelector("#address-to"); + const amount = document.querySelector("#amount"); + const memo = document.querySelector("#memo"); + + transactionErrorMessage.innerHTML = ''; + transactionLinkElement.innerText = ""; + api + .transfer(contractAddress, BlockchainAddress.fromString(to.value), new BN(amount.value, 10), memo.value) + .then(setTransactionLink) + .catch((error) => { + console.error(error); + if (error instanceof TransactionFailedError) { + setTransactionLink(error.putTransaction); + } + transactionErrorMessage.innerText = error; + }); + } +} + +function getBalance() { + const address = getContractAddress(); + const regex = /[0-9A-Fa-f]{42}/g; + const tokenAbi = getTokenApi(); + if (address !== undefined && tokenAbi != undefined) { + const balanceAddress = (document.querySelector("#get-balance-address")).value; + if (balanceAddress.length != 42 || balanceAddress.match(regex) == null) { + // Validate that address is 21 bytes in hexidecimal format + console.error(`${address} is not a valid PBC address`); + } else if (tokenAbi.tokenBalance != undefined) { + const balanceValue = document.querySelector("#balance-value"); + balanceValue.innerHTML = ''; + tokenAbi + .tokenBalance(address, BlockchainAddress.fromString(balanceAddress)) + .then((value) => { + balanceValue.innerHTML = `Value: ${value.toString(10)}`; + }) + .catch((error) => { + console.error(error); + balanceValue.innerText = error; + }); + } + } +} diff --git a/src/main/token/WalletIntegration.ts b/src/main/token/WalletIntegration.ts new file mode 100644 index 0000000..fde0fbe --- /dev/null +++ b/src/main/token/WalletIntegration.ts @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2022 - 2023 Partisia Blockchain Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import { + resetAccount, + setAccount, + getContractAddress, + isConnected, + getContractLastBalanceKey, + setContractLastBalanceKey, + getTokenApi, +} from "./AppState"; +import { ConnectedWallet } from "../shared/ConnectedWallet"; +import { connectMetaMask } from "../shared/MetaMaskIntegration"; +import { connectMpcWallet } from "../shared/MpcWalletIntegration"; +import { connectLedger, validateLedgerConnection } from "../shared/LedgerIntegration"; +import { connectPrivateKey } from "../shared/PrivateKeyIntegration"; +import { CryptoUtils } from "../client/CryptoUtils"; + +/** + * Function for connecting to the MPC wallet and setting the connected wallet in the app state. + */ +export const connectMetaMaskWalletClick = () => { + handleWalletConnect(connectMetaMask()); +}; + +/** + * Function for connecting to the MPC wallet and setting the connected wallet in the app state. + */ +export const connectLedgerWalletClick = () => { + handleWalletConnect(connectLedger()); + setVisibility("#connection-link-ledger-validate", true); +}; + +/** + * Shows the address on the Ledger. + */ +export const validateLedgerConnectionClick = () => { + validateLedgerConnection(); +}; + +/** + * Function for connecting to the MPC wallet and setting the connected wallet in the app state. + */ +export const connectMpcWalletClick = () => { + // Call Partisia SDK to initiate connection + handleWalletConnect(connectMpcWallet()); +}; + +/** + * Connect to the blockchain using a private key. Reads the private key from the form. + */ +export const connectPrivateKeyWalletClick = () => { + const privateKey = document.querySelector("#private-key-value"); + const keyPair = CryptoUtils.privateKeyToKeypair(privateKey.value); + const sender = CryptoUtils.keyPairToAccountAddress(keyPair); + handleWalletConnect(connectPrivateKey(sender, keyPair)); +}; + +/** + * Common code for handling a generic wallet connection. + * @param connect the wallet connection. Can be Mpc Wallet, Metamask, or using a private key. + */ +const handleWalletConnect = (connect: Promise) => { + // Clean up state + resetAccount(); + setConnectionStatus("Connecting..."); + connect + .then((userAccount) => { + setAccount(userAccount); + + // Fix UI + setConnectionStatus("Logged in: ", userAccount.address); + setVisibility("#wallet-connect", false); + setVisibility("#metamask-connect", false); + setVisibility("#ledger-connect", false); + setVisibility("#private-key-connect", false); + setVisibility("#wallet-disconnect", true); + updateInteractionVisibility(); + }) + .catch((error) => { + console.error(error); + if ("message" in error) { + setConnectionStatus(error.message); + } else { + setConnectionStatus("An error occurred trying to connect wallet: " + error); + } + }); +}; + +/** + * Reset state to disconnect current user. + */ +export const disconnectWalletClick = () => { + resetAccount(); + setConnectionStatus("Disconnected account"); + setVisibility("#wallet-connect", true); + setVisibility("#metamask-connect", true); + setVisibility("#ledger-connect", true); + setVisibility("#private-key-connect", true); + setVisibility("#wallet-disconnect", false); + setVisibility("#connection-link-ledger-validate", false); + updateInteractionVisibility(); +}; + +const ALL_BALANCES_LIST = document.querySelector("#all-balances"); + +/** + * Write some of the state to the UI. + */ +export const updateContractState = () => { + const address = getContractAddress(); + if (address === undefined) { + throw new Error("No address provided"); + } + const tokenApi = getTokenApi(); + if (tokenApi === undefined) { + throw new Error("Token API not setup"); + } + + const refreshLoader = document.querySelector("#refresh-loader"); + refreshLoader.classList.remove("hidden"); + + if (tokenApi.basicState != undefined) { + tokenApi.basicState(address).then((state) => { + const stateHeader = document.querySelector("#state-header"); + const updateStateButton = document.querySelector("#update-state"); + stateHeader.classList.remove("hidden"); + updateStateButton.classList.remove("hidden"); + + const name = document.querySelector("#name"); + name.innerHTML = `${state.name}`; + + const decimals = document.querySelector("#decimals"); + decimals.innerHTML = `${state.decimals}`; + + const symbol = document.querySelector("#symbol"); + symbol.innerHTML = `${state.symbol}`; + + const owner = document.querySelector("#owner"); + owner.innerHTML = `${state.owner.asString()}`; + + const totalSupply = document.querySelector("#total-supply"); + totalSupply.innerHTML = `${state.totalSupply}`; + + const contractState = document.querySelector("#contract-state"); + contractState.classList.remove("hidden"); + refreshLoader.classList.add("hidden"); + }); + } + + setContractLastBalanceKey(undefined); + ALL_BALANCES_LIST.innerHTML = ""; + fetchAndDisplayMoreBalances(); +}; + +function setConnectionStatus(status: string, address: string | undefined = undefined) { + const statusText = document.querySelector("#connection-status"); + const statusLink = document.querySelector("#connection-link"); + + statusText.innerHTML = status; + statusLink.innerText = ""; + if (address != undefined) { + statusLink.href = `https://browser.testnet.partisiablockchain.com/accounts/${address}`; + statusLink.innerText = address; + } +} + +const setVisibility = (selector: string, visible: boolean) => { + const element = document.querySelector(selector); + if (visible) { + element.classList.remove("hidden"); + } else { + element.classList.add("hidden"); + } +}; + +export const updateInteractionVisibility = () => { + const contractInteraction = document.querySelector("#contract-interaction"); + if (isConnected() && getContractAddress() !== undefined) { + contractInteraction.classList.remove("hidden"); + } else { + contractInteraction.classList.add("hidden"); + } +}; + +export const fetchAndDisplayMoreBalances = () => { + const address = getContractAddress(); + if (address == undefined) { + throw new Error("Address not set"); + } + const tokenApi = getTokenApi(); + if (tokenApi === undefined) { + throw new Error("Token API not setup"); + } + if (tokenApi.tokenBalances == undefined) { + return; // Contract doesn't support tokenBalances + } + + const lastKey = getContractLastBalanceKey(); + tokenApi.tokenBalances(address, lastKey).then((balancesResult) => { + balancesResult.balances.forEach((amount, tokenOwner) => { + const balance = document.createElement("li"); + balance.innerHTML = `${tokenOwner.asString()}: ${amount}`; + ALL_BALANCES_LIST.appendChild(balance); + }); + setContractLastBalanceKey(balancesResult.next_cursor); + + const loadMoreBtn = document.querySelector("#balances-load-more-btn"); + if (balancesResult.next_cursor === undefined) { + loadMoreBtn.classList.add("hidden"); + } else { + loadMoreBtn.classList.remove("hidden"); + } + }); +}; diff --git a/src/main/token/contract/MpcTokenContract.ts b/src/main/token/contract/MpcTokenContract.ts new file mode 100644 index 0000000..1d878a1 --- /dev/null +++ b/src/main/token/contract/MpcTokenContract.ts @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2022 - 2023 Partisia Blockchain Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import { BlockchainAddress } from "@partisiablockchain/abi-client"; +import BN from "bn.js"; +import { TransactionApi } from "../../client/TransactionApi"; +import { transferWithLargeMemo } from "../../abi/MpcToken"; +import { TokenContract } from "../../shared/TokenContract"; +import { ShardedClient } from "../../client/ShardedClient"; +import { PutTransactionWasSuccessful } from "../../client/TransactionData"; + +export const MPC_TOKEN_CONTRACT_ADDRESS: BlockchainAddress = BlockchainAddress.fromString( + "01a4082d9d560749ecd0ffa1dcaaaee2c2cb25d881" +); + +const GAS_COST_TRANSFER_WITH_LARGE_MEMO = 16_250; + +/*eslint @typescript-eslint/no-unused-vars: ["error", { "argsIgnorePattern": "^unused" }]*/ +export class MpcTokenContract implements TokenContract { + private readonly transactionApi: TransactionApi | undefined; + private readonly shardedClient: ShardedClient; + + constructor(shardedClient: ShardedClient, transactionApi: TransactionApi | undefined) { + this.transactionApi = transactionApi; + this.shardedClient = shardedClient; + } + + /** + * Build and send transfer transaction. + * @param to receiver of tokens + * @param amount number of tokens to send + */ + public transfer( + contractAddress: BlockchainAddress, + to: BlockchainAddress, + amount: BN, + memo: string, + ): Promise { + if (this.transactionApi === undefined) { + throw new Error("No account logged in"); + } + if (contractAddress.asString() !== MPC_TOKEN_CONTRACT_ADDRESS.asString()) { + throw new Error("MpcTokenContract is only supported with the actual MPC_TOKEN_CONTRACT"); + } + + // First build the RPC buffer that is the payload of the transaction. + const rpc = transferWithLargeMemo(to, amount, memo); + // Then send the payload via the transaction API. + // We are sending the transaction to the configured address of the token address, and use the + // GasCost utility to estimate how much the transaction costs. + return this.transactionApi.sendTransactionAndWait(contractAddress.asString(), rpc, GAS_COST_TRANSFER_WITH_LARGE_MEMO); + } +} diff --git a/src/main/token/contract/TokenV1Contract.ts b/src/main/token/contract/TokenV1Contract.ts new file mode 100644 index 0000000..83c9745 --- /dev/null +++ b/src/main/token/contract/TokenV1Contract.ts @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2022 - 2023 Partisia Blockchain Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import { BlockchainAddress } from "@partisiablockchain/abi-client"; +import BN from "bn.js"; +import { TransactionApi } from "../../client/TransactionApi"; +import { transfer, TokenState, deserializeTokenState } from "../../abi/TokenV1"; +import { + TokenContract, + TokenContractBasicState, + TokenBalancesResult, +} from "../../shared/TokenContract"; +import { ShardedClient } from "../../client/ShardedClient"; +import { PutTransactionWasSuccessful } from "../../client/TransactionData"; + +/** + * Structure of the raw data from a WASM contract. + */ +interface RawContractData { + state: { data: string }; +} + +export class TokenV1Contract implements TokenContract { + private readonly transactionApi: TransactionApi | undefined; + private readonly shardedClient: ShardedClient; + + constructor(shardedClient: ShardedClient, transactionApi: TransactionApi | undefined) { + this.transactionApi = transactionApi; + this.shardedClient = shardedClient; + } + + /** + * Build and send transfer transaction. + * @param to receiver of tokens + * @param amount number of tokens to send + */ + public transfer( + contractAddress: BlockchainAddress, + to: BlockchainAddress, + amount: BN, + memo: string, + ): Promise { + if (this.transactionApi === undefined) { + throw new Error("No account logged in"); + } + + // First build the RPC buffer that is the payload of the transaction. + const rpc = transfer(to, amount); + // Then send the payload via the transaction API. + // We are sending the transaction to the configured address of the token address, and use the + // GasCost utility to estimate how much the transaction costs. + return this.transactionApi.sendTransactionAndWait(contractAddress.asString(), rpc, 10_000); + } + + private getState(contractAddress: BlockchainAddress): Promise { + return this.shardedClient + .getContractData(contractAddress.asString()) + .then((contract) => { + if (contract == null) { + throw new Error("Could not find data for contract"); + } + + // Reads the state of the contract + const stateBuffer = Buffer.from(contract.serializedContract.state.data, "base64"); + + return deserializeTokenState({ state: stateBuffer }); + }); + } + + public basicState(contractAddress: BlockchainAddress): Promise { + return this.getState(contractAddress); + } + + /*eslint @typescript-eslint/no-unused-vars: ["error", { "argsIgnorePattern": "^unused" }]*/ + public tokenBalances( + contractAddress: BlockchainAddress, + unusedCursor: string | undefined + ): Promise { + return this.getState(contractAddress).then((state) => { + return { balances: state.balances, next_cursor: undefined }; + }); + } +} diff --git a/src/main/token/contract/TokenV2Contract.ts b/src/main/token/contract/TokenV2Contract.ts new file mode 100644 index 0000000..4ee12b7 --- /dev/null +++ b/src/main/token/contract/TokenV2Contract.ts @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2022 - 2023 Partisia Blockchain Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import BN from "bn.js"; +import { BlockchainAddress } from "@partisiablockchain/abi-client"; +import { TransactionApi } from "../../client/TransactionApi"; +import { AvlClient } from "../../client/AvlClient"; +import { LittleEndianByteInput } from "@secata-public/bitmanipulation-ts"; +import { Buffer } from "buffer"; +import { transfer, deserializeTokenState } from "../../abi/TokenV2"; +import { + TokenContract, + TokenContractBasicState, + TokenBalancesResult, +} from "../../shared/TokenContract"; +import { ShardedClient } from "../../client/ShardedClient"; +import { PutTransactionWasSuccessful } from "../../client/TransactionData"; +import { NODE_BASE_URL, NETWORK_SHARDS } from "../../constant"; + +const AVL_CLIENT = new AvlClient(NODE_BASE_URL, NETWORK_SHARDS); + +/** + * Structure of the raw data from a WASM contract. + */ +interface RawContractData { + state: { data: string }; +} + +/** + * API for the token contract. + * This minimal implementation only allows for transferring tokens to a single address. + * + * The implementation uses the TransactionApi to send transactions, and ABI for the contract to be + * able to build the RPC for the transfer transaction. + */ +export class TokenV2Contract implements TokenContract { + private readonly transactionApi: TransactionApi | undefined; + private readonly shardedClient: ShardedClient; + + constructor(shardedClient: ShardedClient, transactionApi: TransactionApi | undefined) { + this.transactionApi = transactionApi; + this.shardedClient = shardedClient; + } + + /** + * Build and send transfer transaction. + * @param to receiver of tokens + * @param amount number of tokens to send + */ + public transfer( + contractAddress: BlockchainAddress, + to: BlockchainAddress, + amount: BN, + memo: string, + ): Promise { + if (this.transactionApi === undefined) { + throw new Error("No account logged in"); + } + + // First build the RPC buffer that is the payload of the transaction. + const rpc = transfer(to, amount); + // Then send the payload via the transaction API. + // We are sending the transaction to the configured address of the token address, and use the + // GasCost utility to estimate how much the transaction costs. + return this.transactionApi.sendTransactionAndWait(contractAddress.asString(), rpc, 10_000); + } + + public basicState(contractAddress: BlockchainAddress): Promise { + return this.shardedClient + .getContractData(contractAddress.asString()) + .then((contract) => { + if (contract == null) { + throw new Error("Could not find data for contract"); + } + + // Reads the state of the contract + const stateBuffer = Buffer.from(contract.serializedContract.state.data, "base64"); + + return deserializeTokenState({ state: stateBuffer }); + }); + } + + public async tokenBalances( + contractAddress: BlockchainAddress, + keyAddress: string | undefined + ): Promise { + const keyBytes = keyAddress === undefined ? undefined : Buffer.from(keyAddress, "hex"); + const values = await AVL_CLIENT.getContractStateAvlNextN( + contractAddress.asString(), + 0, + keyBytes, + 10 + ); + if (values === undefined) { + throw new Error("Could not fetch balances"); + } + + const balances: Map = new Map(); + let lastKey: string | undefined = undefined; + + values.forEach((val) => { + const tuple = Object.entries(val)[0]; + const blockchainAddressRaw = Buffer.from(tuple[0], "base64").toString("hex"); + const blockchainAddress = BlockchainAddress.fromString(blockchainAddressRaw); + const value = new LittleEndianByteInput( + Buffer.from(tuple[1], "base64") + ).readUnsignedBigInteger(16); + balances.set(blockchainAddress, value); + lastKey = blockchainAddressRaw; + }); + + return { + balances, + next_cursor: lastKey, + }; + } + + public async tokenBalance( + contractAddress: BlockchainAddress, + owner: BlockchainAddress + ): Promise { + const keyBytes = owner.asBuffer(); + const value = await AVL_CLIENT.getContractStateAvlValue( + contractAddress.asString(), + 0, + keyBytes + ); + if (value === undefined) { + throw new Error("No balance for user: " + owner.asString()); + } + return new LittleEndianByteInput(value).readUnsignedBigInteger(16); + } +} diff --git a/src/main/token/index.html b/src/main/token/index.html new file mode 100644 index 0000000..c0fb306 --- /dev/null +++ b/src/main/token/index.html @@ -0,0 +1,234 @@ + + + + MPC20-v2 + + + + + + + + + + + example application + + Account + + Currently not logged in + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Address: + None + + + + + + + + + + + Transfer + + + + + + + + + + + + State + + + + + + + + + + Name + + + + Symbol + + + + Decimals + + + + Total Supply + + + + Owner + + + + Balances + + + + + + All balances + + + + + + + diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..bfc631e --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": ["node_modules"], + "compilerOptions": { + "declaration": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "outDir": "./target", + "target": "es6", + "module": "CommonJS", + "moduleResolution": "Node", + "strict": true, + "sourceMap": true, + "jsx": "react", + "lib": ["dom", "es2015", "es2016", "es2017", "esnext"] + } +} diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..45fd307 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,71 @@ +const webpackConfig = require("webpack"); +const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin"); +const HtmlWebpackPlugin = require("html-webpack-plugin"); +const PathPlugin = require("path"); +const { merge } = require("webpack-merge"); + +function path(path) { + return PathPlugin.join(__dirname, "src", path); +} + +module.exports = (env) => { + const port = env.PORT; + + const configuration = { + mode: "development", + devtool: "eval-cheap-module-source-map", + devServer: { + historyApiFallback: true, + port, + }, + }; + + return merge(configuration, { + entry: { + token: path("main/token/Main"), + }, + resolve: { + alias: { + process: "process/browser", + }, + extensions: [".ts", ".tsx", ".js"], + fallback: { + crypto: require.resolve("crypto-browserify"), + stream: require.resolve("stream-browserify"), + assert: require.resolve("assert"), + }, + }, + output: { + filename: "[name].[chunkhash].js", + path: PathPlugin.join(__dirname, "target"), + publicPath: "/", + }, + module: { + rules: [ + { + test: /\.(ts|tsx)$/, + include: path("main"), + loader: "babel-loader", + options: { + presets: ["@babel/preset-env", "@babel/typescript"], + }, + }, + ], + }, + plugins: [ + new ForkTsCheckerWebpackPlugin({ + typescript: { + configOverwrite: { + compilerOptions: { + noUnusedLocals: false, + noUnusedParameters: false, + }, + }, + }, + }), + new HtmlWebpackPlugin({ filename: 'index.html', template: path("main/index.html"), chunks: [] }), + new HtmlWebpackPlugin({ filename: 'token/index.html',template: path("main/token/index.html"), chunks:["token"] }), + new webpackConfig.ProvidePlugin({ Buffer: ["buffer", "Buffer"], process: "process/browser" }) + ].filter(Boolean) + }); +};
+ Currently not logged in + +