Skip to content

Commit

Permalink
feat: add public wallet (#561)
Browse files Browse the repository at this point in the history
  • Loading branch information
luizstacio committed Oct 31, 2022
1 parent 9b1eeeb commit 0e91213
Show file tree
Hide file tree
Showing 36 changed files with 999 additions and 645 deletions.
8 changes: 8 additions & 0 deletions .changeset/empty-toys-change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@fuel-ts/contract": minor
"@fuel-ts/providers": minor
"typechain-target-fuels": minor
"@fuel-ts/wallet": minor
---

Split Wallet in public and private wallets and enable contracts to use BasicWallet
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ module.exports = {
'no-await-in-loop': 0,
'prefer-destructuring': 0,
'no-bitwise': 0,
'no-underscore-dangle': 'off',
'class-methods-use-this': 'off',
'@typescript-eslint/no-inferrable-types': 'off',
'@typescript-eslint/lines-between-class-members': [
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@
"eslint-config-airbnb-typescript": "^16.2.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"forc-bin": "workspace:*",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsdoc": "^37.9.7",
"eslint-plugin-jsx-a11y": "^6.6.1",
Expand All @@ -67,6 +66,7 @@
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-tsdoc": "^0.2.17",
"ethers": "^5.7.2",
"forc-bin": "workspace:*",
"jest": "28.1.0",
"markdownlint": "^0.23.1",
"markdownlint-cli": "^0.27.1",
Expand Down
8 changes: 4 additions & 4 deletions packages/contract/src/contracts/contract-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { CreateTransactionRequestLike } from '@fuel-ts/providers';
import { Provider, CreateTransactionRequest } from '@fuel-ts/providers';
import type { StorageSlot } from '@fuel-ts/transactions';
import { MAX_GAS_PER_TX } from '@fuel-ts/transactions';
import { Wallet } from '@fuel-ts/wallet';
import { BaseWalletLocked } from '@fuel-ts/wallet/src/base-locked-wallet';

import { getContractId, getContractStorageRoot, includeHexPrefix } from '../util';

Expand All @@ -25,12 +25,12 @@ export default class ContractFactory {
bytecode: BytesLike;
interface: Interface;
provider!: Provider | null;
wallet!: Wallet | null;
wallet!: BaseWalletLocked | null;

constructor(
bytecode: BytesLike,
abi: JsonAbi | Interface,
walletOrProvider: Wallet | Provider | null = null
walletOrProvider: BaseWalletLocked | Provider | null = null
) {
this.bytecode = bytecode;

Expand All @@ -40,7 +40,7 @@ export default class ContractFactory {
this.interface = new Interface(abi);
}

if (walletOrProvider instanceof Wallet) {
if (walletOrProvider instanceof BaseWalletLocked) {
this.provider = walletOrProvider.provider;
this.wallet = walletOrProvider;
} else if (walletOrProvider instanceof Provider) {
Expand Down
8 changes: 4 additions & 4 deletions packages/contract/src/contracts/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Interface } from '@fuel-ts/abi-coder';
import { Address } from '@fuel-ts/address';
import type { AbstractAddress, AbstractContract } from '@fuel-ts/interfaces';
import type { Provider } from '@fuel-ts/providers';
import { Wallet } from '@fuel-ts/wallet';
import { BaseWalletLocked } from '@fuel-ts/wallet';

import type { InvokeFunctions } from '../types';

Expand All @@ -14,18 +14,18 @@ export default class Contract implements AbstractContract {
id!: AbstractAddress;
provider!: Provider | null;
interface!: Interface;
wallet!: Wallet | null;
wallet!: BaseWalletLocked | null;
functions: InvokeFunctions = {};

constructor(
id: string | AbstractAddress,
abi: JsonAbi | JsonFlatAbi | Interface,
walletOrProvider: Wallet | Provider | null = null
walletOrProvider: BaseWalletLocked | Provider | null = null
) {
this.interface = abi instanceof Interface ? abi : new Interface(abi);
this.id = Address.fromAddressOrString(id);

if (walletOrProvider instanceof Wallet) {
if (walletOrProvider instanceof BaseWalletLocked) {
this.provider = walletOrProvider.provider;
this.wallet = walletOrProvider;
} else {
Expand Down
26 changes: 19 additions & 7 deletions packages/contract/src/contracts/functions/base-invocation-scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import type { InputValue } from '@fuel-ts/abi-coder';
import type { ContractIdLike } from '@fuel-ts/interfaces';
import { bn, toNumber } from '@fuel-ts/math';
import type { Provider, CoinQuantity } from '@fuel-ts/providers';
import type { Provider, CoinQuantity, TransactionRequest } from '@fuel-ts/providers';
import { transactionRequestify, ScriptTransactionRequest } from '@fuel-ts/providers';
import { MAX_GAS_PER_TX, InputType } from '@fuel-ts/transactions';

Expand Down Expand Up @@ -184,6 +184,18 @@ export class BaseInvocationScope<TReturn = any> {
return this;
}

/**
* Prepare transaction request object, adding Inputs, Outputs, coins, check gas costs
* and transaction validity.
*
* It's possible to get the transaction without adding coins, by passing `fundTransaction`
* as false.
*/
async getTransactionRequest(options?: CallOptions): Promise<TransactionRequest> {
await this.prepareTransaction(options);
return this.transactionRequest;
}

/**
* Submits a transaction to the blockchain.
*
Expand All @@ -194,8 +206,8 @@ export class BaseInvocationScope<TReturn = any> {
async call<T = TReturn>(options?: CallOptions): Promise<FunctionInvocationResult<T>> {
assert(this.contract.wallet, 'Wallet is required!');

await this.prepareTransaction(options);
const response = await this.contract.wallet.sendTransaction(this.transactionRequest);
const transactionRequest = await this.getTransactionRequest(options);
const response = await this.contract.wallet.sendTransaction(transactionRequest);

return FunctionInvocationResult.build<T>(
this.functionInvocationScopes,
Expand All @@ -215,8 +227,8 @@ export class BaseInvocationScope<TReturn = any> {
async simulate<T = TReturn>(options?: CallOptions): Promise<InvocationCallResult<T>> {
assert(this.contract.wallet, 'Wallet is required!');

await this.prepareTransaction(options);
const result = await this.contract.wallet.simulateTransaction(this.transactionRequest);
const transactionRequest = await this.getTransactionRequest(options);
const result = await this.contract.wallet.simulateTransaction(transactionRequest);

return InvocationCallResult.build<T>(this.functionInvocationScopes, result, this.isMultiCall);
}
Expand All @@ -233,8 +245,8 @@ export class BaseInvocationScope<TReturn = any> {
const provider = (this.contract.wallet?.provider || this.contract.provider) as Provider;
assert(provider, 'Wallet or Provider is required!');

await this.prepareTransaction(options);
const request = transactionRequestify(this.transactionRequest);
const transactionRequest = await this.getTransactionRequest(options);
const request = transactionRequestify(transactionRequest);
const response = await provider.call(request, {
utxoValidation: false,
});
Expand Down
89 changes: 88 additions & 1 deletion packages/fuel-gauge/src/contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ import {
Provider,
TestUtils,
Contract,
transactionRequestify,
FunctionInvocationResult,
Wallet,
} from 'fuels';
import type { BN } from 'fuels';
import type { BN, TransactionRequestLike, TransactionResponse } from 'fuels';
import { join } from 'path';

import abiJSON from '../test-projects/call-test-contract/out/debug/call-test-abi.json';
Expand Down Expand Up @@ -576,4 +579,88 @@ describe('Contract', () => {
.get();
}).rejects.toThrow();
});

it('Parse TX to JSON and parse back to TX', async () => {
const contract = await setupContract();

const num = 1337;
const struct = { a: true, b: 1337 };
const invocationScopes = [contract.functions.foo(num), contract.functions.boo(struct)];
const multiCallScope = contract.multiCall(invocationScopes);

const transactionRequest = await multiCallScope.getTransactionRequest();

const txRequest = JSON.stringify(transactionRequest);
const txRequestParsed = JSON.parse(txRequest);

const transactionRequestParsed = transactionRequestify(txRequestParsed);

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const response = await contract.wallet!.sendTransaction(transactionRequestParsed);
const {
value: [resultA, resultB],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} = await FunctionInvocationResult.build<any>(invocationScopes, response, true, contract);

expect(resultA.toHex()).toEqual(bn(num).add(1).toHex());
expect(resultB.a).toEqual(!struct.a);
expect(resultB.b.toHex()).toEqual(bn(struct.b).add(1).toHex());
});

it('Provide a custom provider and public wallet to the contract instance', async () => {
const contract = await setupContract();
const externalWallet = Wallet.generate();
await TestUtils.seedWallet(externalWallet, [
{
amount: bn(1_000_000_000),
assetId: NativeAssetId,
},
]);

// Create a custom provider to emulate a external signer
// like Wallet Extension or a Hardware wallet
let signedTransaction;
class ProviderCustom extends Provider {
async sendTransaction(
transactionRequestLike: TransactionRequestLike
): Promise<TransactionResponse> {
const transactionRequest = transactionRequestify(transactionRequestLike);
// Simulate a external request of signature
signedTransaction = await externalWallet.signTransaction(transactionRequest);
transactionRequest.updateWitnessByOwner(externalWallet.address, signedTransaction);
return super.sendTransaction(transactionRequestLike);
}
}

// Set custom provider to contract instance
const customProvider = new ProviderCustom('http://127.0.0.1:4000/graphql');
contract.wallet = Wallet.fromAddress(externalWallet.address, customProvider);
contract.provider = customProvider;

const num = 1337;
const struct = { a: true, b: 1337 };
const invocationScopes = [contract.functions.foo(num), contract.functions.boo(struct)];
const multiCallScope = contract.multiCall(invocationScopes);

const transactionRequest = await multiCallScope.getTransactionRequest();

const txRequest = JSON.stringify(transactionRequest);
const txRequestParsed = JSON.parse(txRequest);

const transactionRequestParsed = transactionRequestify(txRequestParsed);

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const response = await contract.wallet!.sendTransaction(transactionRequestParsed);
const {
value: [resultA, resultB],
transactionResponse,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} = await FunctionInvocationResult.build<any>(invocationScopes, response, true, contract);

expect(transactionResponse.request.witnesses.length).toEqual(1);
expect(transactionResponse.request.witnesses[0]).toEqual(signedTransaction);
expect(resultA.toHex()).toEqual(bn(num).add(1).toHex());
expect(resultB.a).toEqual(!struct.a);
expect(resultB.b.toHex()).toEqual(bn(struct.b).add(1).toHex());
});
});
4 changes: 2 additions & 2 deletions packages/fuel-gauge/src/coverage-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,11 +339,11 @@ describe('Coverage Contract', () => {
it('should get initial state messages from node', async () => {
const provider = new Provider('http://127.0.0.1:4000/graphql');

const WALLET_A = new Wallet(
const WALLET_A = Wallet.fromPrivateKey(
'0x1ff16505df75735a5bcf4cb4cf839903120c181dd9be6781b82cda23543bd242',
provider
);
const WALLET_B = new Wallet(
const WALLET_B = Wallet.fromPrivateKey(
'0x30bb0bc68f5d2ec3b523cee5a65503031b40679d9c72280cd8088c2cfbc34e38',
provider
);
Expand Down
6 changes: 3 additions & 3 deletions packages/fuel-gauge/src/predicate.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { readFileSync } from 'fs';
import { Address, NativeAssetId, bn, toHex, toNumber, Provider, TestUtils, Predicate } from 'fuels';
import type { AbstractAddress, BigNumberish, BN, Wallet } from 'fuels';
import type { AbstractAddress, BigNumberish, BN, Wallet, BaseWalletLocked } from 'fuels';
import { join } from 'path';

import testPredicateAddress from '../test-projects/predicate-address';
Expand All @@ -20,7 +20,7 @@ const setup = async () => {
};

const setupPredicate = async (
wallet: Wallet,
wallet: BaseWalletLocked,
amountToPredicate: BigNumberish,
predicate: Predicate
): Promise<BN> => {
Expand All @@ -31,7 +31,7 @@ const setupPredicate = async (
};

const assertResults = async (
wallet: Wallet,
wallet: BaseWalletLocked,
receiverAddress: AbstractAddress,
initialPredicateBalance: BN,
initialReceiverBalance: BN,
Expand Down
2 changes: 1 addition & 1 deletion packages/fuel-gauge/src/token-test-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const setup = async () => {
};

describe('TokenTestContract', () => {
it.only('Can mint and transfer coins', async () => {
it('Can mint and transfer coins', async () => {
// New wallet to transfer coins and check balance
const userWallet = Wallet.generate({ provider });
const token = await setup();
Expand Down
4 changes: 2 additions & 2 deletions packages/fuel-gauge/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { readFileSync } from 'fs';
import type { Interface, JsonAbi, Wallet, Contract, BytesLike } from 'fuels';
import type { Interface, JsonAbi, Contract, BytesLike, WalletUnlocked } from 'fuels';
import { NativeAssetId, Provider, TestUtils, ContractFactory } from 'fuels';
import { join } from 'path';

Expand All @@ -10,7 +10,7 @@ const deployContract = async (factory: ContractFactory, useCache: boolean = true
return contractInstance;
};

let walletInstance: Wallet;
let walletInstance: WalletUnlocked;
const createWallet = async () => {
if (walletInstance) return walletInstance;
const provider = new Provider('http://127.0.0.1:4000/graphql');
Expand Down
17 changes: 17 additions & 0 deletions packages/providers/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,23 @@ export default class Provider {
};
}

/**
* Executes a signed transaction without applying the states changes
* on the chain.
*/
async simulate(transactionRequestLike: TransactionRequestLike): Promise<CallResult> {
const transactionRequest = transactionRequestify(transactionRequestLike);
const encodedTransaction = hexlify(transactionRequest.toTransactionBytes());
const { dryRun: gqlReceipts } = await this.operations.dryRun({
encodedTransaction,
utxoValidation: true,
});
const receipts = gqlReceipts.map(processGqlReceipt);
return {
receipts,
};
}

/**
* Returns a transaction cost to enable user
* to set gasLimit and also reserve balance amounts
Expand Down
13 changes: 10 additions & 3 deletions packages/providers/src/transaction-request/transaction-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import type { Coin } from '../coin';
import type { CoinQuantity, CoinQuantityLike } from '../coin-quantity';
import { coinQuantityfy } from '../coin-quantity';
import type { Message } from '../message';
import { calculatePriceWithFactor } from '../util';
import { arraifyFromUint8Array, calculatePriceWithFactor } from '../util';

import type {
CoinTransactionRequestOutput,
Expand Down Expand Up @@ -177,6 +177,13 @@ abstract class BaseTransactionRequest implements BaseTransactionRequestLike {
return this.witnesses.length - 1;
}

updateWitnessByOwner(address: AbstractAddress, signature: BytesLike) {
const witnessIndex = this.getCoinInputWitnessIndexByOwner(address);
if (typeof witnessIndex === 'number') {
this.updateWitness(witnessIndex, signature);
}
}

/**
* Updates an existing witness without any side effects
*/
Expand Down Expand Up @@ -382,8 +389,8 @@ export class ScriptTransactionRequest extends BaseTransactionRequest {

constructor({ script, scriptData, ...rest }: ScriptTransactionRequestLike = {}) {
super(rest);
this.script = arrayify(script ?? returnZeroScript.bytes);
this.scriptData = arrayify(scriptData ?? returnZeroScript.encodeScriptData());
this.script = arraifyFromUint8Array(script ?? returnZeroScript.bytes);
this.scriptData = arraifyFromUint8Array(scriptData ?? returnZeroScript.encodeScriptData());
}

toTransaction(): Transaction {
Expand Down
Loading

1 comment on commit 0e91213

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage report

St.
Category Percentage Covered / Total
🟢 Statements 90.14% 3556/3945
🟡 Branches 72.52% 694/957
🟢 Functions 87.99% 718/816
🟢 Lines 90.15% 3406/3778

Test suite run success

541 tests passing in 49 suites.

Report generated by 🧪jest coverage report action from 0e91213

Please sign in to comment.