Skip to content

Commit

Permalink
Flow -enable any type of transaction to be signed and broadcasted to …
Browse files Browse the repository at this point in the history
…the blockchain
  • Loading branch information
Samuel Sramko committed Sep 22, 2021
1 parent e193f51 commit aa042ea
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 64 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tatumio/tatum",
"version": "1.25.11",
"version": "1.26.0",
"description": "Tatum API client allows browsers and Node.js clients to interact with Tatum API.",
"main": "dist/src/index.js",
"repository": "https://github.com/tatumio/tatum-js",
Expand Down
63 changes: 63 additions & 0 deletions src/model/request/TransferFlowCustomTx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {Type} from 'class-transformer';
import {IsIn, IsNotEmpty, IsOptional, Length, ValidateNested} from 'class-validator';
import {FlowMnemonicOrPrivateKeyOrSignatureId} from './FlowMnemonicOrPrivateKeyOrSignatureId';

const types = ['Identity',
'UInt',
'Int',
'UInt8',
'Int8',
'UInt16',
'Int16',
'UInt32',
'Int32',
'UInt64',
'Int64',
'UInt128',
'Int128',
'UInt256',
'Int256',
'Word8',
'Word16',
'Word32',
'Word64',
'UFix64',
'Fix64',
'String',
'Character',
'Bool',
'Address',
'Void',
'Optional',
'Reference',
'Array',
'Dictionary',
'Event',
'Resource',
'Struct'];

export class FlowArgs {

@IsNotEmpty()
public value: string | string[];

@IsNotEmpty()
@IsIn(types)
public type: string;

@IsOptional()
@IsIn(types)
public subType?: string;
}

export class TransferFlowCustomTx extends FlowMnemonicOrPrivateKeyOrSignatureId {

@IsNotEmpty()
@Length(1, 500000)
public transaction: string;

@IsOptional()
@ValidateNested({each: true})
@Type(() => FlowArgs)
public args: FlowArgs[];
}
7 changes: 4 additions & 3 deletions src/model/request/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ export * from './TransferBchBlockchain'
export * from './TransferBtcBasedBlockchain'
export * from './TransferCustomErc20'
export * from './TransferErc20'
export * from './TransferEthErc20'
export * from './OneBurn721'
export * from './TransferEthErc20';
export * from './TransferFlowCustomTx';
export * from './OneBurn721';
export * from './OneBurnMultiToken'
export * from './OneBurnMultiTokenBatch'
export * from './OneDeploy20'
Expand Down Expand Up @@ -142,4 +143,4 @@ export * from './UpdateCashbackErc721'
export * from './PrivateKeyOrSignatureIdBtcBased'
export {ContractType} from './ContractType'
export * from './egld'
export * from './AlgoTransaction'
export * from './AlgoTransaction'
63 changes: 41 additions & 22 deletions src/transaction/flow.spec.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import {Currency, TransferFlow} from '../model'
import {flowAddPublicKeyToAccount, flowCreateAccountFromPublicKey, flowSendTransaction, getFlowNftMetadata, getFlowNftTokenByAddress} from './flow'
import dedent from 'dedent-js';
import {Currency, TransferFlow, TransferFlowCustomTx} from '../model';
import {flowAddPublicKeyToAccount, flowCreateAccountFromPublicKey, flowSendCustomTransaction, flowSendTransaction, getFlowNftMetadata, getFlowNftTokenByAddress} from './flow';

describe('Flow tests', () => {

jest.setTimeout(99999)
jest.setTimeout(99999);

it('should create account from public key', async () => {
const result = await flowCreateAccountFromPublicKey(true, '968c3ce11e871cb2b7161b282655ee5fcb051f3c04894705d771bf11c6fbebfc6556ab8a0c04f45ea56281312336d0668529077c9d66891a6cad3db877acbe90', '0x955cd3f17b2fd8ad', '37afa218d41d9cd6a2c6f2b96d9eaa3ad96c598252bc50e4d45d62f9356a51f8')
console.log(result)
expect(result.address).toBeDefined()
expect(result.txId).toBeDefined()
const result = await flowCreateAccountFromPublicKey(true, '968c3ce11e871cb2b7161b282655ee5fcb051f3c04894705d771bf11c6fbebfc6556ab8a0c04f45ea56281312336d0668529077c9d66891a6cad3db877acbe90', '0x955cd3f17b2fd8ad', '37afa218d41d9cd6a2c6f2b96d9eaa3ad96c598252bc50e4d45d62f9356a51f8');
console.log(result);
expect(result.address).toBeDefined();
expect(result.txId).toBeDefined();
})

it('should add public key to account', async () => {
Expand All @@ -20,23 +21,41 @@ describe('Flow tests', () => {
})

it('should send FLOW transaction', async () => {
const body = new TransferFlow()
body.to = '0x21cbd745a4df66f1'
body.amount = '1'
body.account = '0x955cd3f17b2fd8ad'
body.privateKey = '37afa218d41d9cd6a2c6f2b96d9eaa3ad96c598252bc50e4d45d62f9356a51f8'
body.currency = Currency.FLOW
const result = await flowSendTransaction(true, body)
expect(result.txId).toBeDefined()
})
const body = new TransferFlow();
body.to = '0x21cbd745a4df66f1';
body.amount = '1';
body.account = '0x955cd3f17b2fd8ad';
body.privateKey = '37afa218d41d9cd6a2c6f2b96d9eaa3ad96c598252bc50e4d45d62f9356a51f8';
body.currency = Currency.FLOW;
const result = await flowSendTransaction(true, body);
expect(result.txId).toBeDefined();
});

it('should send FLOW custom transaction', async () => {
const body = new TransferFlowCustomTx();
body.transaction = dedent`transaction(publicKey: String) {
prepare(signer: AuthAccount) {
signer.addPublicKey(publicKey.decodeHex())
}
}`;
body.args = [{
value: 'f847b840968c3ce11e871cb2b7161b282655ee5fcb051f3c04894705d771bf11c6fbebfc6556ab8a0c04f45ea56281312336d0668529077c9d66891a6cad3db877acbe9003038203e8',
type: 'String'
}];
body.account = '0x955cd3f17b2fd8ad';
body.privateKey = '37afa218d41d9cd6a2c6f2b96d9eaa3ad96c598252bc50e4d45d62f9356a51f8';
const result = await flowSendCustomTransaction(true, body);
expect(result.txId).toBeDefined();
expect(result.events[0].data.address).toBe('0x955cd3f17b2fd8ad');
});

it('should get NFT token by address', async () => {
const result = await getFlowNftTokenByAddress(true, '0x2d0d7b39db4e3a08', '27320939-3087-490e-a65e-a53c8b06fcd9')
expect(result).toBeDefined()
})
const result = await getFlowNftTokenByAddress(true, '0x2d0d7b39db4e3a08', '27320939-3087-490e-a65e-a53c8b06fcd9');
expect(result).toBeDefined();
});

it('should get NFT token metadata', async () => {
const result = await getFlowNftMetadata(true, '0x2d0d7b39db4e3a08', '8', '27320939-3087-490e-a65e-a53c8b06fcd9')
expect(result).toBeDefined()
})
const result = await getFlowNftMetadata(true, '0x2d0d7b39db4e3a08', '8', '27320939-3087-490e-a65e-a53c8b06fcd9');
expect(result).toBeDefined();
});
})
97 changes: 59 additions & 38 deletions src/transaction/flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import * as elliptic from 'elliptic';
import {SHA3} from 'sha3';
import {validateBody} from '../connector/tatum';
import {FLOW_MAINNET_ADDRESSES, FLOW_TESTNET_ADDRESSES} from '../constants';
import {Currency, FlowBurnNft, FlowMintMultipleNft, FlowMintNft, FlowTransferNft, TransactionKMS, TransferFlow} from '../model';
import {Currency, FlowArgs, FlowBurnNft, FlowMintMultipleNft, FlowMintNft, FlowTransferNft, TransactionKMS, TransferFlow, TransferFlowCustomTx} from '../model';
import {generatePrivateKeyFromMnemonic} from '../wallet';
import {
burnFlowNftTokenTxTemplate,
Expand All @@ -31,6 +31,7 @@ export enum FlowTxType {
MINT_MULTIPLE_NFT,
BURN_NFT,
TRANSFER_NFT,
CUSTOM_TX,
}

interface Account {
Expand All @@ -51,15 +52,9 @@ interface AccountAuthorization {
}


type Argument = {
type: string,
subType?: string,
value: string | string[]
}

type Transaction = {
code: string
args: Argument[]
args: FlowArgs[]
proposer: AccountAuthorizer
authorizations: AccountAuthorizer[]
payer: AccountAuthorizer
Expand Down Expand Up @@ -141,14 +136,14 @@ const sendTransaction = async (testnet: boolean, {
}
}

const sendScript = async (testnet: boolean, code: string, args: Argument[]) => {
fcl.config().put('accessNode.api', testnet ? 'https://access-testnet.onflow.org' : 'https://access-mainnet-beta.onflow.org')
const sendScript = async (testnet: boolean, code: string, args: FlowArgs[]) => {
fcl.config().put('accessNode.api', testnet ? 'https://access-testnet.onflow.org' : 'https://access-mainnet-beta.onflow.org');
const response = await fcl.send([
fcl.script(code),
fcl.args(args.map(arg => fcl.arg(arg.type === 'UInt64' ? parseInt(arg.value as string) : arg.value, types[arg.type]))),
])
return fcl.decode(response)
}
]);
return fcl.decode(response);
};

export const flowSignKMSTransaction = async (tx: TransactionKMS, privateKeys: string[], testnet: boolean) => {
if (tx.chain !== Currency.FLOW) {
Expand All @@ -161,15 +156,17 @@ export const flowSignKMSTransaction = async (tx: TransactionKMS, privateKeys: st
case FlowTxType.ADD_PK_TO_ACCOUNT:
return await flowAddPublicKeyToAccount(testnet, body.publicKey, body.account, privateKeys[0])
case FlowTxType.TRANSFER:
return await flowSendTransaction(testnet, {...body, privateKey: privateKeys[0]})
return await flowSendTransaction(testnet, {...body, privateKey: privateKeys[0]});
case FlowTxType.TRANSFER_NFT:
return await sendFlowNftTransferToken(testnet, {...body, privateKey: privateKeys[0]})
return await sendFlowNftTransferToken(testnet, {...body, privateKey: privateKeys[0]});
case FlowTxType.MINT_NFT:
return await sendFlowNftMintToken(testnet, {...body, privateKey: privateKeys[0]})
return await sendFlowNftMintToken(testnet, {...body, privateKey: privateKeys[0]});
case FlowTxType.MINT_MULTIPLE_NFT:
return await sendFlowNftMintMultipleToken(testnet, {...body, privateKey: privateKeys[0]})
return await sendFlowNftMintMultipleToken(testnet, {...body, privateKey: privateKeys[0]});
case FlowTxType.BURN_NFT:
return await sendFlowNftBurnToken(testnet, {...body, privateKey: privateKeys[0]})
return await sendFlowNftBurnToken(testnet, {...body, privateKey: privateKeys[0]});
default:
return await flowSendCustomTransaction(testnet, {...body, privateKey: privateKeys[0]});
}
}

Expand Down Expand Up @@ -226,7 +223,7 @@ export const getFlowNftTokenByAddress = async (testnet: boolean, account: string
* This operation is irreversible.
* @param testnet
* @param body content of the transaction to broadcast
* @returns transaction id of the transaction in the blockchain
* @returns txId id of the transaction in the blockchain
*/
export const sendFlowNftMintToken = async (testnet: boolean, body: FlowMintNft):
Promise<{ txId: string, tokenId: string }> => {
Expand All @@ -248,7 +245,7 @@ export const sendFlowNftMintToken = async (testnet: boolean, body: FlowMintNft):
* This operation is irreversible.
* @param testnet
* @param body content of the transaction to broadcast
* @returns transaction id of the transaction in the blockchain
* @returns txId id of the transaction in the blockchain
*/
export const sendFlowNftMintMultipleToken = async (testnet: boolean, body: FlowMintMultipleNft):
Promise<{ txId: string, tokenId: number[] }> => {
Expand All @@ -270,19 +267,19 @@ export const sendFlowNftMintMultipleToken = async (testnet: boolean, body: FlowM
* This operation is irreversible.
* @param testnet
* @param body content of the transaction to broadcast
* @returns transaction id of the transaction in the blockchain
* @returns {txId: string, events: any[]} id of the transaction in the blockchain and events this tx produced
*/
export const sendFlowNftTransferToken = async (testnet: boolean, body: FlowTransferNft):
Promise<{ txId: string }> => {
await validateBody(body, FlowTransferNft)
const code = transferFlowNftTokenTxTemplate(testnet)
const {tokenId, to, mnemonic, index, account, privateKey} = body
const args = [{type: 'Address', value: to}, {type: 'UInt64', value: tokenId}]
const pk = (mnemonic && index && index >= 0) ? await generatePrivateKeyFromMnemonic(Currency.FLOW, testnet, mnemonic, index as number) : privateKey as string
const auth = getFlowSigner(pk, account)
const result = await sendTransaction(testnet, {code, args, proposer: auth, authorizations: [auth], payer: auth})
await validateBody(body, FlowTransferNft);
const code = transferFlowNftTokenTxTemplate(testnet);
const {tokenId, to, mnemonic, index, account, privateKey} = body;
const args = [{type: 'Address', value: to}, {type: 'UInt64', value: tokenId}];
const pk = (mnemonic && index && index >= 0) ? await generatePrivateKeyFromMnemonic(Currency.FLOW, testnet, mnemonic, index as number) : privateKey as string;
const auth = getFlowSigner(pk, account);
const result = await sendTransaction(testnet, {code, args, proposer: auth, authorizations: [auth], payer: auth});
if (result.error) {
throw new Error(result.error)
throw new Error(result.error);
}
return {txId: result.id}
}
Expand All @@ -292,23 +289,47 @@ export const sendFlowNftTransferToken = async (testnet: boolean, body: FlowTrans
* This operation is irreversible.
* @param testnet
* @param body content of the transaction to broadcast
* @returns transaction id of the transaction in the blockchain
* @returns txId id of the transaction in the blockchain
*/
export const sendFlowNftBurnToken = async (testnet: boolean, body: FlowBurnNft):
Promise<{ txId: string }> => {
await validateBody(body, FlowBurnNft)
const code = burnFlowNftTokenTxTemplate(testnet)
const {tokenId, contractAddress: tokenType, mnemonic, index, account, privateKey} = body
const args = [{type: 'UInt64', value: tokenId}, {type: 'String', value: tokenType}]
const pk = (mnemonic && index && index >= 0) ? await generatePrivateKeyFromMnemonic(Currency.FLOW, testnet, mnemonic, index as number) : privateKey as string
const auth = getFlowSigner(pk, account)
const result = await sendTransaction(testnet, {code, args, proposer: auth, authorizations: [auth], payer: auth})
const code = burnFlowNftTokenTxTemplate(testnet);
const {tokenId, contractAddress: tokenType, mnemonic, index, account, privateKey} = body;
const args = [{type: 'UInt64', value: tokenId}, {type: 'String', value: tokenType}];
const pk = (mnemonic && index && index >= 0) ? await generatePrivateKeyFromMnemonic(Currency.FLOW, testnet, mnemonic, index as number) : privateKey as string;
const auth = getFlowSigner(pk, account);
const result = await sendTransaction(testnet, {code, args, proposer: auth, authorizations: [auth], payer: auth});
if (result.error) {
throw new Error(result.error)
throw new Error(result.error);
}
return {txId: result.id}
return {txId: result.id};
}

/**
* Send custom transaction to the FLOW network
* @param testnet
* @param body content of the transaction to broadcast
* @returns txId id of the transaction in the blockchain
*/
export const flowSendCustomTransaction = async (testnet: boolean, body: TransferFlowCustomTx):
Promise<{ txId: string, events: any[] }> => {
await validateBody(body, TransferFlowCustomTx);
const pk = body.privateKey || await generatePrivateKeyFromMnemonic(Currency.FLOW, testnet, body.mnemonic as string, body.index as number);
const auth = getFlowSigner(pk, body.account);
const result = await sendTransaction(testnet, {code: body.transaction, args: body.args, proposer: auth, authorizations: [auth], payer: auth});
if (result.error) {
throw new Error(result.error);
}
return {txId: result.id, events: result.events};
};

/**
* Send FLOW or FUSD from account to account.
* @param testnet
* @param body content of the transaction to broadcast
* @returns txId id of the transaction in the blockchain
*/
export const flowSendTransaction = async (testnet: boolean, body: TransferFlow):
Promise<{ txId: string }> => {
await validateBody(body, TransferFlow)
Expand Down

0 comments on commit aa042ea

Please sign in to comment.