Basic UI on example web client
This commit is contained in:
parent
816be5391f
commit
215bfd1576
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
.idea/
|
||||
*.iml
|
||||
target/
|
||||
node_modules/
|
27
README.md
27
README.md
|
@ -1 +1,28 @@
|
|||
# Defi UI TODO
|
||||
|
||||
TODO
|
||||
|
||||
Demo for how to integrate PBC wallet into a web frontend or dApp.
|
||||
|
||||
## Requirements
|
||||
|
||||
To be able to run the demo the following setup is required.
|
||||
|
||||
- node.js version v.16.15.0 or newer.
|
||||
|
||||
## How to run?
|
||||
|
||||
First type check the Typescript using.
|
||||
|
||||
```shell
|
||||
npm ts
|
||||
```
|
||||
|
||||
To run the example run
|
||||
|
||||
```shell
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
and view the demo at localhost:8080
|
||||
|
|
56
package.json
Normal file
56
package.json
Normal 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
398
src/main/abi/MpcToken.ts
Normal 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
228
src/main/abi/TokenV1.ts
Normal 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
246
src/main/abi/TokenV2.ts
Normal 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();
|
||||
}
|
26
src/main/client/AccountData.ts
Normal file
26
src/main/client/AccountData.ts
Normal 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;
|
||||
}
|
73
src/main/client/AvlClient.ts
Normal file
73
src/main/client/AvlClient.ts
Normal 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];
|
||||
}
|
||||
}
|
61
src/main/client/BaseClient.ts
Normal file
61
src/main/client/BaseClient.ts
Normal 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);
|
||||
}
|
66
src/main/client/BufferWriter.ts
Normal file
66
src/main/client/BufferWriter.ts
Normal 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]);
|
||||
};
|
||||
}
|
33
src/main/client/ContractData.ts
Normal file
33
src/main/client/ContractData.ts
Normal 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 };
|
204
src/main/client/CryptoUtils.ts
Normal file
204
src/main/client/CryptoUtils.ts
Normal 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,
|
||||
};
|
60
src/main/client/PbcClient.ts
Normal file
60
src/main/client/PbcClient.ts
Normal 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);
|
||||
}
|
||||
}
|
112
src/main/client/ShardedClient.ts
Normal file
112
src/main/client/ShardedClient.ts
Normal 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));
|
||||
}
|
||||
}
|
120
src/main/client/TransactionApi.ts
Normal file
120
src/main/client/TransactionApi.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
85
src/main/client/TransactionData.ts
Normal file
85
src/main/client/TransactionData.ts
Normal 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;
|
||||
}
|
43
src/main/client/TransactionSerialization.ts
Normal file
43
src/main/client/TransactionSerialization.ts
Normal 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
4
src/main/constant.ts
Normal 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
48
src/main/index.html
Normal 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>
|
184
src/main/pbc-ledger-client/PbcLedgerClient.ts
Normal file
184
src/main/pbc-ledger-client/PbcLedgerClient.ts
Normal 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)),
|
||||
};
|
||||
}
|
||||
}
|
22
src/main/pbc-ledger-client/bip32-path.d.ts
vendored
Normal file
22
src/main/pbc-ledger-client/bip32-path.d.ts
vendored
Normal 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 {}
|
42
src/main/shared/ConnectedWallet.ts
Normal file
42
src/main/shared/ConnectedWallet.ts
Normal 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>;
|
||||
}
|
145
src/main/shared/LedgerIntegration.ts
Normal file
145
src/main/shared/LedgerIntegration.ts
Normal 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);
|
||||
};
|
117
src/main/shared/MetaMaskIntegration.ts
Normal file
117
src/main/shared/MetaMaskIntegration.ts
Normal 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");
|
||||
}
|
||||
};
|
107
src/main/shared/MpcWalletIntegration.ts
Normal file
107
src/main/shared/MpcWalletIntegration.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
};
|
80
src/main/shared/PrivateKeyIntegration.ts
Normal file
80
src/main/shared/PrivateKeyIntegration.ts
Normal 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 };
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
79
src/main/shared/TokenContract.ts
Normal file
79
src/main/shared/TokenContract.ts
Normal 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>;
|
||||
}
|
99
src/main/token/AppState.ts
Normal file
99
src/main/token/AppState.ts
Normal 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
205
src/main/token/Main.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
231
src/main/token/WalletIntegration.ts
Normal file
231
src/main/token/WalletIntegration.ts
Normal 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");
|
||||
}
|
||||
});
|
||||
};
|
68
src/main/token/contract/MpcTokenContract.ts
Normal file
68
src/main/token/contract/MpcTokenContract.ts
Normal 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);
|
||||
}
|
||||
}
|
98
src/main/token/contract/TokenV1Contract.ts
Normal file
98
src/main/token/contract/TokenV1Contract.ts
Normal 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 };
|
||||
});
|
||||
}
|
||||
}
|
148
src/main/token/contract/TokenV2Contract.ts
Normal file
148
src/main/token/contract/TokenV2Contract.ts
Normal 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
234
src/main/token/index.html
Normal 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
17
tsconfig.json
Normal 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
71
webpack.config.js
Normal 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)
|
||||
});
|
||||
};
|
Loading…
Reference in New Issue
Block a user