1
0

Basic UI on example web client

This commit is contained in:
Jon Michael Aanes 2024-06-08 15:41:36 +02:00
parent 816be5391f
commit 215bfd1576
Signed by: Jmaa
SSH Key Fingerprint: SHA256:Ab0GfHGCblESJx7JRE4fj4bFy/KRpeLhi41y4pF3sNA
36 changed files with 3841 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.idea/
*.iml
target/
node_modules/

View File

@ -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

56
package.json Normal file
View File

@ -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
}
}

398
src/main/abi/MpcToken.ts Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
*/
/* 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> = K | undefined;
export interface MpcTokenContractState {
icedStakings: Option<Map<Option<Hash>, Option<IceStaking>>>;
locked: boolean;
stakeDelegations: Option<Map<Option<Hash>, Option<StakeDelegation>>>;
transfers: Option<Map<Option<Hash>, Option<TransferInformation>>>;
}
export function newMpcTokenContractState(
icedStakings: Option<Map<Option<Hash>, Option<IceStaking>>>,
locked: boolean,
stakeDelegations: Option<Map<Option<Hash>, Option<StakeDelegation>>>,
transfers: Option<Map<Option<Hash>, Option<TransferInformation>>>
): 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<BlockchainAddress>;
sender: Option<BlockchainAddress>;
}
export function newIceStaking(
amount: BN,
contract: Option<BlockchainAddress>,
sender: Option<BlockchainAddress>
): 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<DelegationType>;
recipient: Option<BlockchainAddress>;
sender: Option<BlockchainAddress>;
}
export function newStakeDelegation(
amount: BN,
delegationType: Option<DelegationType>,
recipient: Option<BlockchainAddress>,
sender: Option<BlockchainAddress>
): 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<BN>;
recipient: Option<BlockchainAddress>;
sender: Option<BlockchainAddress>;
symbol: Option<string>;
}
export function newTransferInformation(
amount: Option<BN>,
recipient: Option<BlockchainAddress>,
sender: Option<BlockchainAddress>,
symbol: Option<string>
): 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();
}

228
src/main/abi/TokenV1.ts Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
*/
/* 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> = 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<BlockchainAddress, BN>;
allowed: Map<BlockchainAddress, Map<BlockchainAddress, BN>>;
}
export function newTokenState(
name: string,
decimals: number,
symbol: string,
owner: BlockchainAddress,
totalSupply: BN,
balances: Map<BlockchainAddress, BN>,
allowed: Map<BlockchainAddress, Map<BlockchainAddress, BN>>
): 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();
}

246
src/main/abi/TokenV2.ts Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
*/
/* 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> = 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<Map<BlockchainAddress, BN>>;
allowed: Option<Map<AllowedAddress, BN>>;
}
export function newTokenState(
name: string,
decimals: number,
symbol: string,
owner: BlockchainAddress,
totalSupply: BN,
balances: Option<Map<BlockchainAddress, BN>>,
allowed: Option<Map<AllowedAddress, BN>>
): 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();
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*
*/
/**
* Interface to specify some values of a PBC account.
* The nonce is needed when building transactions.
*/
export interface AccountData {
address: string;
nonce: number;
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*
*/
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<Buffer | undefined> {
return getRequest<Buffer>(this.contractStateQueryUrl(address) + "?stateOutput=binary");
}
public async getContractStateAvlValue(
address: string,
treeId: number,
key: Buffer
): Promise<Buffer | undefined> {
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<Array<Record<string, string>> | undefined> {
if (key === undefined) {
return getRequest<Array<Record<string, string>>>(
`${this.contractStateQueryUrl(address)}/avl/${treeId}/next?n=${n}`
);
} else {
return getRequest<Array<Record<string, string>>>(
`${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];
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*
*/
// 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<T>(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<R>(url: string): Promise<R | undefined> {
const options = buildOptions("GET", getHeaders, null);
return handleFetch(fetch(url, options));
}
export function putRequest<R, T>(url: string, object: T): Promise<R | undefined> {
const options = buildOptions("PUT", postHeaders, object);
return handleFetch(fetch(url, options));
}
function handleFetch<T>(promise: Promise<Response>): Promise<T | undefined> {
return promise
.then((response) => {
if (response.status === 200) {
return response.json();
} else {
return undefined;
}
})
.catch(() => undefined);
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*
*/
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]);
};
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*
*/
/**
* 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<T> = ContractCore & { serializedContract: T };

View File

@ -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 <http://www.gnu.org/licenses/>.
*
*/
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,
};

View File

@ -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 <http://www.gnu.org/licenses/>.
*
*/
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<T>(
address: string,
withState = true
): Promise<ContractCore | ContractData<T> | undefined> {
const query = "?requireContractState=" + withState;
return getRequest(this.host + "/blockchain/contracts/" + address + query);
}
public getAccountData(address: string): Promise<AccountData | undefined> {
return getRequest<AccountData>(this.host + "/blockchain/account/" + address).then(
(response?: AccountData) => {
if (response != null) {
response.address = address;
}
return response;
}
);
}
public getExecutedTransaction(
identifier: string,
requireFinal = true
): Promise<ExecutedTransactionDto | undefined> {
const query = "?requireFinal=" + requireFinal;
return getRequest(this.host + "/blockchain/transaction/" + identifier + query);
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*
*/
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<AccountData | undefined> {
return this.clientForAddress(address).getAccountData(address);
}
public getContractData<T>(
address: string,
withState?: true
): Promise<ContractData<T> | undefined>;
public getContractData<T>(
address: string,
withState?: boolean
): Promise<ContractData<T> | 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<ExecutedTransactionDto | undefined> {
return this.getClient(shard).getExecutedTransaction(identifier, requireFinal);
}
public putTransaction(transaction: Buffer): Promise<TransactionPointer | undefined> {
const byteJson = { payload: transaction.toString("base64") };
return putRequest(this.baseUrl + "/chain/transactions", byteJson);
}
private clientForAddress(address: string) {
return this.getClient(this.shardForAddress(address));
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*
*/
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<PutTransactionWasSuccessful> {
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<unknown> => {
return new Promise((resolve) => setTimeout(resolve, millis));
};
private readonly waitForTransaction = (
shard: ShardId,
identifier: string,
originalTransaction: PutTransactionWasSuccessful,
tryCount = 0
): Promise<void> => {
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);
}
});
};
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*
*/
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<PayloadT> = 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;
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*
*/
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<Rpc>
): 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));
}

4
src/main/constant.ts Normal file
View File

@ -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";

48
src/main/index.html Normal file
View File

@ -0,0 +1,48 @@
<!doctype html>
<html lang="en" xmlns="http://www.w3.org/1999/html">
<head>
<title>PBC wallet integration</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="/conf/config.js"></script>
<style>
.hidden {
display: none;
}
body > .pure-g {
max-width: 1200px;
width: 1200px;
margin: auto;
}
div {
margin-bottom: 5px;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/purecss@3.0.0/build/pure-min.css"
integrity="sha384-X38yfunGUhNzHpBaEBsWLO+A0HDYOQi8ufWDkZ0k9e0eXz/tH3II7uKZ9msv++Ls"
crossorigin="anonymous" />
</head>
<body>
<div class="pure-g">
<div class="pure-u-1-1">
<h1>Example Clients</h1>
<a class="pure-button pure-button-primary" href="token?mpc20-v1">MPC20-V1 Token Contract</a>
<a class="pure-button pure-button-primary" href="token?mpc20-v2">MPC20-V2 Token Contract</a>
<a class="pure-button pure-button-primary" href="token?mpc-token">MPC Token</a>
</div>
</div>
</body>
</html>

View File

@ -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 <http://www.gnu.org/licenses/>.
*
*/
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<string> {
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<Signature> {
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)),
};
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*
*/
declare module "bip32-path";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
class BIPPath {}

View File

@ -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 <http://www.gnu.org/licenses/>.
*
*/
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<Rpc>,
cost?: string | number
) => Promise<ShardPutTransactionResponse>;
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*
*/
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<Rpc>,
cost: string | number,
ledgerClient: PbcLedgerClient,
senderAddress: string
): Promise<ShardPutTransactionResponse> {
// 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<ConnectedWallet> => {
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<Rpc>, 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<void> => {
const ledgerTransport = await TransportWebUSB.create();
//const ledgerTransport = await TransportWebHID.create();
const ledgerClient = new PbcLedgerClient(ledgerTransport);
await ledgerClient.getAddress(DEFAULT_KEYPATH, true);
};

View File

@ -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 <http://www.gnu.org/licenses/>.
*
*/
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<string, unknown>;
}
interface MetaMask {
request<T>(args: MetamaskRequestArguments): Promise<T>;
}
/**
* 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<ConnectedWallet> => {
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");
}
};

View File

@ -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 <http://www.gnu.org/licenses/>.
*
*/
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<ConnectedWallet> => {
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);
}
});
};

View File

@ -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 <http://www.gnu.org/licenses/>.
*
*/
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<ConnectedWallet> => {
return {
address: sender,
signAndSendTransaction: (client: ShardedClient, payload: TransactionPayload<Rpc>, 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 };
}
});
});
},
};
};

View File

@ -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 <http://www.gnu.org/licenses/>.
*
*/
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<BlockchainAddress, BN>;
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<PutTransactionWasSuccessful>;
/**
* Determines the basic state of the contract.
*/
readonly basicState?: (contractAddress: BlockchainAddress) => Promise<TokenContractBasicState>;
/**
* Fetch a specific balance
*/
readonly tokenBalance?: (
contractAddress: BlockchainAddress,
owner: BlockchainAddress
) => Promise<BN>;
/**
* Iterator for fetching token balances.
*/
readonly tokenBalances?: (
contractAddress: BlockchainAddress,
cursor: string | undefined
) => Promise<TokenBalancesResult>;
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*
*/
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;
};

205
src/main/token/Main.ts Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
*/
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 = <Element>document.querySelector("#wallet-connect-btn");
connectWallet.addEventListener("click", connectMpcWalletClick);
// Setup event listener to connect to the MetaMask snap
const metaMaskConnect = <Element>document.querySelector("#metamask-connect-btn");
metaMaskConnect.addEventListener("click", connectMetaMaskWalletClick);
// Setup event listener to connect to the ledger snap
const ledgerConnect = <Element>document.querySelector("#ledger-connect-btn");
ledgerConnect.addEventListener("click", connectLedgerWalletClick);
const ledgerConnectValidate = <Element>document.querySelector("#connection-link-ledger-validate");
ledgerConnectValidate.addEventListener("click", validateLedgerConnectionClick);
// Setup event listener to login using private key
const pkConnect = <Element>document.querySelector("#private-key-connect-btn");
pkConnect.addEventListener("click", connectPrivateKeyWalletClick);
// Setup event listener to drop the connection again
const disconnectWallet = <Element>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 = <Element>document.querySelector("#transfer-btn");
transferBtn.addEventListener("click", transferAction);
const addressBtn = <Element>document.querySelector("#address-btn");
addressBtn.addEventListener("click", contractAddressClick);
const updateStateBtn = <Element>document.querySelector("#update-state-btn");
updateStateBtn.addEventListener("click", updateContractState);
const getBalanceBtn = <Element>document.querySelector("#get-balance-btn");
getBalanceBtn.addEventListener("click", getBalance);
const loadMoreBtn = <Element>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 = <HTMLElement>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 = <HTMLAnchorElement>document.querySelector("#current-address");
const inputAddress = <HTMLInputElement>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 = (<HTMLInputElement>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 = <HTMLInputElement>document.querySelector("#sign-transaction-error");
const transactionLinkElement = <HTMLInputElement>document.querySelector("#sign-transaction-link");
function setTransactionLink(transaction: PutTransactionWasSuccessful) {
const transactionLinkElement = <HTMLInputElement>document.querySelector("#sign-transaction-link");
transactionLinkElement.innerHTML = `<a href="${BROWSER_BASE_URL}/transactions/${transaction.transactionHash}" target="_blank">Transaction link in browser</a>`;
transactionErrorMessage.innerText = "";
}
/** Action for the sign petition button */
function transferAction() {
const api = getTokenApi();
const contractAddress = getContractAddress();
if (isConnected() && api !== undefined && contractAddress !== undefined) {
const to = <HTMLInputElement>document.querySelector("#address-to");
const amount = <HTMLInputElement>document.querySelector("#amount");
const memo = <HTMLInputElement>document.querySelector("#memo");
transactionErrorMessage.innerHTML = '<div class="loader"></div>';
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 = (<HTMLInputElement>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 = <HTMLInputElement>document.querySelector("#balance-value");
balanceValue.innerHTML = '<br><div class="loader"></div>';
tokenAbi
.tokenBalance(address, BlockchainAddress.fromString(balanceAddress))
.then((value) => {
balanceValue.innerHTML = `<br>Value: ${value.toString(10)}`;
})
.catch((error) => {
console.error(error);
balanceValue.innerText = error;
});
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*
*/
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 = <HTMLInputElement>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<ConnectedWallet>) => {
// 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 = <HTMLElement>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 = <HTMLInputElement>document.querySelector("#refresh-loader");
refreshLoader.classList.remove("hidden");
if (tokenApi.basicState != undefined) {
tokenApi.basicState(address).then((state) => {
const stateHeader = <HTMLInputElement>document.querySelector("#state-header");
const updateStateButton = <HTMLInputElement>document.querySelector("#update-state");
stateHeader.classList.remove("hidden");
updateStateButton.classList.remove("hidden");
const name = <HTMLElement>document.querySelector("#name");
name.innerHTML = `${state.name}`;
const decimals = <HTMLElement>document.querySelector("#decimals");
decimals.innerHTML = `${state.decimals}`;
const symbol = <HTMLElement>document.querySelector("#symbol");
symbol.innerHTML = `${state.symbol}`;
const owner = <HTMLElement>document.querySelector("#owner");
owner.innerHTML = `${state.owner.asString()}`;
const totalSupply = <HTMLElement>document.querySelector("#total-supply");
totalSupply.innerHTML = `${state.totalSupply}`;
const contractState = <HTMLElement>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 = <HTMLElement>document.querySelector("#connection-status");
const statusLink = <HTMLAnchorElement>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 = <HTMLElement>document.querySelector(selector);
if (visible) {
element.classList.remove("hidden");
} else {
element.classList.add("hidden");
}
};
export const updateInteractionVisibility = () => {
const contractInteraction = <HTMLElement>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 = `<span class="address">${tokenOwner.asString()}</span>: ${amount}`;
ALL_BALANCES_LIST.appendChild(balance);
});
setContractLastBalanceKey(balancesResult.next_cursor);
const loadMoreBtn = <Element>document.querySelector("#balances-load-more-btn");
if (balancesResult.next_cursor === undefined) {
loadMoreBtn.classList.add("hidden");
} else {
loadMoreBtn.classList.remove("hidden");
}
});
};

View File

@ -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 <http://www.gnu.org/licenses/>.
*
*/
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<PutTransactionWasSuccessful> {
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);
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*
*/
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<PutTransactionWasSuccessful> {
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<TokenState> {
return this.shardedClient
.getContractData<RawContractData>(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<TokenContractBasicState> {
return this.getState(contractAddress);
}
/*eslint @typescript-eslint/no-unused-vars: ["error", { "argsIgnorePattern": "^unused" }]*/
public tokenBalances(
contractAddress: BlockchainAddress,
unusedCursor: string | undefined
): Promise<TokenBalancesResult> {
return this.getState(contractAddress).then((state) => {
return { balances: state.balances, next_cursor: undefined };
});
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*
*/
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<PutTransactionWasSuccessful> {
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<TokenContractBasicState> {
return this.shardedClient
.getContractData<RawContractData>(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<TokenBalancesResult> {
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<BlockchainAddress, BN> = 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<BN> {
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);
}
}

234
src/main/token/index.html Normal file
View File

@ -0,0 +1,234 @@
<!doctype html>
<html lang="en">
<head>
<title>MPC20-v2</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="/conf/config.js"></script>
<style>
.hidden {
display: none;
}
body > .pure-g {
max-width: 1200px;
width: 1200px;
margin: auto;
}
div {
margin-bottom: 5px;
}
.loader {
border: 4px solid #f3f3f3; /* Light grey */
border-top: 4px solid #3498db; /* Blue */
border-radius: 50%;
width: 20px;
height: 20px;
animation: spin 2s linear infinite;
}
.input-address {
width: 56ex;
font-family: monospace;
}
.address {
font-family: monospace;
font-size: 1.3em;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/purecss@3.0.0/build/pure-min.css"
integrity="sha384-X38yfunGUhNzHpBaEBsWLO+A0HDYOQi8ufWDkZ0k9e0eXz/tH3II7uKZ9msv++Ls"
crossorigin="anonymous" />
</head>
<body>
<div class="pure-g">
<div class="pure-u-1-1">
<h1><span class="mode"></span> example application</h1>
<div>
<h2>Account</h2>
<p>
<span id="connection-status">Currently not logged in</span>
<a id="connection-link" target="_blank" class="address"></a>
</p>
<div>
<form class="pure-form" onSubmit="return false;">
<input
class="pure-button pure-button-primary hidden"
id="connection-link-ledger-validate"
type="submit"
value="Verify on Ledger" />
</form>
</div>
<div id="private-key-connect">
<form class="pure-form" onSubmit="return false;">
<input
class="pure-button pure-button-primary"
id="private-key-connect-btn"
type="submit"
value="Login using private key" />
<input id="private-key-value" name="private-key-value" type="password" maxlength="" />
</form>
</div>
<div id="wallet-connect">
<form>
<input
class="pure-button pure-button-primary"
id="wallet-connect-btn"
type="button"
value="Login using MPC Wallet" />
</form>
</div>
<div id="metamask-connect">
<form>
<input
class="pure-button pure-button-primary"
id="metamask-connect-btn"
type="button"
value="Login using MetaMask snap" />
</form>
</div>
<div id="ledger-connect">
<form>
<input
class="pure-button pure-button-primary"
id="ledger-connect-btn"
type="button"
value="Login using PBC Ledger App" />
</form>
</div>
<div id="wallet-disconnect" class="hidden">
<form>
<input
class="pure-button pure-button-primary"
id="wallet-disconnect-btn"
type="button"
value="Logout" />
</form>
</div>
</div>
<h2>
<span class="mode"></span> Address:
<a id="current-address" target="_blank" class="address">None</a>
</h2>
<div id="address">
<form class="pure-form" onSubmit="return false;">
<input
id="address-value"
class="input-address address"
name="address-value"
type="text"
minlength="42"
maxlength="42" />
<input
class="pure-button pure-button-primary"
id="address-btn"
type="submit"
value="Set contract" />
</form>
</div>
<br />
<div id="contract-interaction" class="hidden">
<h2>Transfer</h2>
<form class="pure-form" name="transfer-action-form" onSubmit="return false;">
<input id="address-to" name="address-to" type="text" minlength="42" maxlength="42" placeholder="Address" />
<input id="amount" name="amount" type="number" min="0" placeholder="MPC Amount" />
<input id="memo" name="memo" type="text" placeholder="Large Memo Text"/>
<input
class="pure-button pure-button-primary"
id="transfer-btn"
type="submit"
value="Transfer" />
</form>
<div id="sign-transaction-link"></div>
<div id="sign-transaction-error"></div>
</div>
<div id="contract-state" class="hidden">
<h2 id="state-header"><span class="mode"></span> State</h2>
<div id="update-state">
<form>
<input
class="pure-button pure-button-primary"
id="update-state-btn"
type="button"
value="Refresh State" />
<input id="refresh-loader" class="loader hidden" />
</form>
</div>
<table>
<tr>
<th>Name</th>
<td id="name"></td>
</tr>
<tr>
<th>Symbol</th>
<td id="symbol"></td>
</tr>
<tr>
<th>Decimals</th>
<td id="decimals"></td>
</tr>
<tr>
<th>Total Supply</th>
<td id="total-supply"></td>
</tr>
<tr>
<th>Owner</th>
<td id="owner" class="address"></td>
</tr>
</table>
<h2>Balances</h2>
<form
class="pure-form"
name="transfer-action-form"
onSubmit="return false;"
id="get-balance-form">
<input
id="get-balance-address"
name="get-balance-address"
class="input-address address"
type="text"
minlength="42"
maxlength="42" />
<input
class="pure-button pure-button-primary"
id="get-balance-btn"
type="submit"
value="Get Balance" />
</form>
<div id="balance-value"></div>
<h3>All balances</h3>
<ul id="all-balances"></ul>
<input
class="pure-button pure-button-primary"
id="balances-load-more-btn"
type="submit"
value="Load More" />
</div>
</div>
</div>
</body>
</html>

17
tsconfig.json Normal file
View File

@ -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"]
}
}

71
webpack.config.js Normal file
View File

@ -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)
});
};