Skip to content

Commit

Permalink
feat: add declare method (#159)
Browse files Browse the repository at this point in the history
* feat: add declare method
  • Loading branch information
stanleyyconsensys authored Nov 10, 2023
1 parent 0b7a8b1 commit 538704e
Show file tree
Hide file tree
Showing 7 changed files with 275 additions and 2 deletions.
26 changes: 26 additions & 0 deletions packages/starknet-snap/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,18 @@ <h1>Hello, Snaps!</h1>
<input type="submit" id="verifyTypedDataSignature" value="Verify Signature" />
</fieldset>
</form>
<form id="declareContract">
<fieldset>
<legend>Declare Contract</legend>
<label for="declareContract_senderAddress">Sender Address</label>
<input type="text" id="declareContract_senderAddress" name="declareContract_senderAddress" /><br />
<label for="declareContract_contract">Contract payload</label>
<input type="text" id="declareContract_contract" name="declareContract_contract" /><br />
<label for="declareContract_invocationsDetails">InvocationsDetails</label>
<input type="text" id="declareContract_invocationsDetails" name="declareContract_invocationsDetails" /><br />
<input type="submit" id="declareContract" value="Declare Contract" />
</fieldset>
</form>
</body>

<script type="module">
Expand Down Expand Up @@ -681,6 +693,20 @@ <h1>Hello, Snaps!</h1>
});
}

async function declareContract(e) {
e.preventDefault(); // to prevent default form behavior

const senderAddress = document.getElementById('declareContract_senderAddress').value;
const contractPayload = document.getElementById('declareContract_contract').value;
const invocationsDetails = document.getElementById('declareContract_invocationsDetails').value;

await callSnap('starkNet_declareContract', {
senderAddress,
contractPayload: contractPayload ? JSON.parse(contractPayload) : contractPayload,
invocationsDetails: invocationsDetails ? JSON.parse(invocationsDetails) : invocationsDetails,
});
}

async function callSnap(method, params) {
try {
const chainId = document.getElementById('targetChainId').value;
Expand Down
48 changes: 48 additions & 0 deletions packages/starknet-snap/src/declareContract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { toJson } from './utils/serializer';
import { ApiParams, DeclareContractRequestParams } from './types/snapApi';
import { getNetworkFromChainId, getDeclareSnapTxt } from './utils/snapUtils';
import { getKeysFromAddress, declareContract as declareContractUtil } from './utils/starknetUtils';
import { DialogType } from '@metamask/rpc-methods';
import { heading, panel } from '@metamask/snaps-ui';
import { logger } from './utils/logger';

export async function declareContract(params: ApiParams) {
try {
const { state, keyDeriver, requestParams, wallet } = params;
const requestParamsObj = requestParams as DeclareContractRequestParams;

logger.log(`executeTxn params: ${toJson(requestParamsObj, 2)}}`);

const senderAddress = requestParamsObj.senderAddress;
const network = getNetworkFromChainId(state, requestParamsObj.chainId);
const { privateKey } = await getKeysFromAddress(keyDeriver, network, state, senderAddress);

const snapComponents = getDeclareSnapTxt(
senderAddress,
network,
requestParamsObj.contractPayload,
requestParamsObj.invocationsDetails,
);

const response = await wallet.request({
method: 'snap_dialog',
params: {
type: DialogType.Confirmation,
content: panel([heading('Do you want to sign this transaction?'), ...snapComponents]),
},
});

if (!response) return false;

return await declareContractUtil(
network,
senderAddress,
privateKey,
requestParamsObj.contractPayload,
requestParamsObj.invocationsDetails,
);
} catch (err) {
logger.error(`Problem found: ${err}`);
throw err;
}
}
5 changes: 5 additions & 0 deletions packages/starknet-snap/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { Mutex } from 'async-mutex';
import { OnRpcRequestHandler } from '@metamask/snaps-types';
import { ApiParams, ApiRequestParams } from './types/snapApi';
import { estimateAccDeployFee } from './estimateAccountDeployFee';
import { declareContract } from './declareContract';
import { logger } from './utils/logger';

declare const snap;
Expand Down Expand Up @@ -165,6 +166,10 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ origin, request }) =>
apiParams.keyDeriver = await getAddressKeyDeriver(snap);
return recoverAccounts(apiParams);

case 'starkNet_declareContract':
apiParams.keyDeriver = await getAddressKeyDeriver(snap);
return declareContract(apiParams);

default:
throw new Error('Method not found.');
}
Expand Down
9 changes: 8 additions & 1 deletion packages/starknet-snap/src/types/snapApi.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { BIP44AddressKeyDeriver } from '@metamask/key-tree';
import Mutex from 'async-mutex/lib/Mutex';
import { SnapState, VoyagerTransactionType } from './snapState';
import { Abi, Call, InvocationsSignerDetails } from 'starknet';
import { Abi, Call, InvocationsSignerDetails, DeclareContractPayload, InvocationsDetails } from 'starknet';

export interface ApiParams {
state: SnapState;
Expand Down Expand Up @@ -31,6 +31,7 @@ export type ApiRequestParams =
| GetStoredTransactionsRequestParams
| GetTransactionsRequestParams
| RecoverAccountsRequestParams
| DeclareContractRequestParams
| SignTransactionParams;

export interface BaseRequestParams {
Expand Down Expand Up @@ -144,6 +145,12 @@ export interface RecoverAccountsRequestParams extends BaseRequestParams {
maxMissed?: string | number;
}

export interface DeclareContractRequestParams extends BaseRequestParams {
senderAddress: string;
contractPayload: DeclareContractPayload;
invocationsDetails?: InvocationsDetails;
}

export interface RpcV4GetTransactionReceiptResponse {
execution_status?: string;
finality_status?: string;
Expand Down
53 changes: 52 additions & 1 deletion packages/starknet-snap/src/utils/snapUtils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { toJson } from './serializer';
import { Mutex } from 'async-mutex';
import { num, Abi, InvocationsSignerDetails, Call } from 'starknet';
import { num, InvocationsDetails, DeclareContractPayload, Abi, InvocationsSignerDetails, Call } from 'starknet';
import { validateAndParseAddress } from './starknetUtils';
import { Component, text, copyable } from '@metamask/snaps-ui';
import {
Expand Down Expand Up @@ -272,6 +272,57 @@ export function getSignTxnTxt(
return components;
}

export function getDeclareSnapTxt(
senderAddress: string,
network: Network,
contractPayload: DeclareContractPayload,
invocationsDetails?: InvocationsDetails,
) {
const components = [];
components.push(text('**Network:**'));
components.push(copyable(network.name));
components.push(text('**Signer Address:**'));
components.push(copyable(senderAddress));

if (contractPayload.contract) {
components.push(text('**Contract:**'));
if (typeof contractPayload.contract === 'string' || contractPayload.contract instanceof String) {
components.push(copyable(contractPayload.contract.toString()));
} else {
components.push(copyable(JSON.stringify(contractPayload.contract, null, 2)));
}
}

if (contractPayload.compiledClassHash) {
components.push(text('**Complied Class Hash:**'));
components.push(copyable(contractPayload.compiledClassHash));
}

if (contractPayload.classHash) {
components.push(text('**Class Hash:**'));
components.push(copyable(contractPayload.classHash));
}

if (contractPayload.casm) {
components.push(text('**Casm:**'));
components.push(copyable(JSON.stringify(contractPayload.casm, null, 2)));
}

if (invocationsDetails?.maxFee !== undefined) {
components.push(text('**Max Fee(ETH):**'));
components.push(copyable(convert(invocationsDetails.maxFee, 'wei', 'ether')));
}
if (invocationsDetails?.nonce !== undefined) {
components.push(text('**Nonce:**'));
components.push(copyable(invocationsDetails.nonce.toString()));
}
if (invocationsDetails?.version !== undefined) {
components.push(text('**Version:**'));
components.push(copyable(invocationsDetails.version.toString()));
}
return components;
}

export function getAddTokenText(
tokenAddress: string,
tokenName: string,
Expand Down
15 changes: 15 additions & 0 deletions packages/starknet-snap/src/utils/starknetUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ import {
GetTransactionResponse,
Invocations,
validateAndParseAddress as _validateAndParseAddress,
DeclareContractPayload,
DeclareContractResponse,
InvocationsDetails,
Signer,
Signature,
stark,
Expand Down Expand Up @@ -78,6 +81,18 @@ export const callContract = async (
);
};

export const declareContract = async (
network: Network,
senderAddress: string,
privateKey: string | Uint8Array,
contractPayload: DeclareContractPayload,
transactionsDetail?: InvocationsDetails,
): Promise<DeclareContractResponse> => {
const provider = getProvider(network);
const account = new Account(provider, senderAddress, privateKey);
return account.declare(contractPayload, transactionsDetail);
};

export const estimateFee = async (
network: Network,
senderAddress: string,
Expand Down
121 changes: 121 additions & 0 deletions packages/starknet-snap/test/src/declareContract.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import chai, { expect } from 'chai';
import sinon from 'sinon';
import sinonChai from 'sinon-chai';
import { WalletMock } from '../wallet.mock.test';
import * as utils from '../../src/utils/starknetUtils';
import { declareContract } from '../../src/declareContract';
import { SnapState } from '../../src/types/snapState';
import { STARKNET_MAINNET_NETWORK, STARKNET_TESTNET_NETWORK } from '../../src/utils/constants';
import { createAccountProxyTxn, getBip44EntropyStub, account1 } from '../constants.test';
import { getAddressKeyDeriver } from '../../src/utils/keyPair';
import { Mutex } from 'async-mutex';
import { ApiParams, DeclareContractRequestParams } from '../../src/types/snapApi';

chai.use(sinonChai);
const sandbox = sinon.createSandbox();

describe('Test function: declareContract', function () {
this.timeout(10000);
const walletStub = new WalletMock();
const state: SnapState = {
accContracts: [],
erc20Tokens: [],
networks: [STARKNET_MAINNET_NETWORK, STARKNET_TESTNET_NETWORK],
transactions: [],
};
const apiParams: ApiParams = {
state,
requestParams: {},
wallet: walletStub,
saveMutex: new Mutex(),
};

const requestObject: DeclareContractRequestParams = {
chainId: STARKNET_MAINNET_NETWORK.chainId,
senderAddress: account1.address,
contractPayload: {
contract: 'TestContract',
},
invocationsDetails: {
maxFee: 100,
},
};

beforeEach(async function () {
walletStub.rpcStubs.snap_getBip44Entropy.callsFake(getBip44EntropyStub);
apiParams.keyDeriver = await getAddressKeyDeriver(walletStub);
apiParams.requestParams = requestObject;
sandbox.useFakeTimers(createAccountProxyTxn.timestamp);
walletStub.rpcStubs.snap_dialog.resolves(true);
walletStub.rpcStubs.snap_manageState.resolves(state);
});

afterEach(function () {
walletStub.reset();
sandbox.restore();
});

it('should declareContract correctly', async function () {
const declareContractStub = sandbox.stub(utils, 'declareContract').resolves({
transaction_hash: 'transaction_hash',
class_hash: 'class_hash',
});
const result = await declareContract(apiParams);
const { privateKey } = await utils.getKeysFromAddress(
apiParams.keyDeriver,
STARKNET_MAINNET_NETWORK,
state,
account1.address,
);

expect(result).to.eql({
transaction_hash: 'transaction_hash',
class_hash: 'class_hash',
});
expect(declareContractStub).to.have.been.calledOnce;
expect(declareContractStub).to.have.been.calledWith(
STARKNET_MAINNET_NETWORK,
account1.address,
privateKey,
{ contract: 'TestContract' },
{ maxFee: 100 },
);
});

it('should throw error if declareContract fail', async function () {
const declareContractStub = sandbox.stub(utils, 'declareContract').rejects('error');
const { privateKey } = await utils.getKeysFromAddress(
apiParams.keyDeriver,
STARKNET_MAINNET_NETWORK,
state,
account1.address,
);
let result;
try {
await declareContract(apiParams);
} catch (e) {
result = e;
} finally {
expect(result).to.be.an('Error');
expect(declareContractStub).to.have.been.calledOnce;
expect(declareContractStub).to.have.been.calledWith(
STARKNET_MAINNET_NETWORK,
account1.address,
privateKey,
{ contract: 'TestContract' },
{ maxFee: 100 },
);
}
});

it('should return false if user rejected to sign the transaction', async function () {
walletStub.rpcStubs.snap_dialog.resolves(false);
const declareContractStub = sandbox.stub(utils, 'declareContract').resolves({
transaction_hash: 'transaction_hash',
class_hash: 'class_hash',
});
const result = await declareContract(apiParams);
expect(result).to.equal(false);
expect(declareContractStub).to.have.been.not.called;
});
});

0 comments on commit 538704e

Please sign in to comment.