Skip to content

Commit

Permalink
saving my place
Browse files Browse the repository at this point in the history
  • Loading branch information
lastmjs committed Mar 21, 2024
1 parent 74ea683 commit 34514e6
Show file tree
Hide file tree
Showing 7 changed files with 226 additions and 82 deletions.
203 changes: 132 additions & 71 deletions examples/ic_evm_rpc/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
// TODO try instantly on base

import { ic, jsonStringify, Server } from 'azle';
import { ethers } from 'ethers';
import express, { Request } from 'express';

import { canisterAddress, chainId } from './globals';
import { ethFeeHistory } from './json_rpc_methods/eth_fee_history';
import { canisterAddress } from './globals';
import { ethGetBalance } from './json_rpc_methods/eth_get_balance';
import { ethGetTransactionCount } from './json_rpc_methods/eth_get_transaction_count';
import { ethMaxPriorityFeePerGas } from './json_rpc_methods/eth_max_priority_fee_per_gas';
import { ethSendRawTransaction } from './json_rpc_methods/eth_send_raw_transaction';
import { ecdsaPublicKey } from './tecdsa/ecdsa_public_key';
import { signWithEcdsa } from './tecdsa/sign_with_ecdsa';
import { calculateRsvForTEcdsa } from './tecdsa/calculate_rsv_for_tecdsa';
Expand Down Expand Up @@ -49,101 +47,164 @@ export default Server(() => {
async (req: Request<any, any, { to: string; value: string }>, res) => {
// address: 0x9Ac70EE21bE697173b74aF64399d850038697FD3
const wallet = new ethers.Wallet(
'0x6f784763681eb712dc16714b8ade23f6c982a5872d054059dd64d0ec4e52be33'
'0x6f784763681eb712dc16714b8ade23f6c982a5872d054059dd64d0ec4e52be33',
// ethers.getDefaultProvider('sepolia')
ethers.getDefaultProvider('https://sepolia.base.org')
);

const to = req.body.to;
const value = ethers.parseEther(req.body.value);
const maxPriorityFeePerGas = await ethMaxPriorityFeePerGas();
const baseFeePerGas = BigInt(
(await ethFeeHistory()).Consistent?.Ok[0].baseFeePerGas[0]
);
const maxFeePerGas = baseFeePerGas * 2n + maxPriorityFeePerGas;
const gasLimit = 21_000n;
const nonce = await ethGetTransactionCount(wallet.address);
const rawTransaction = await wallet.signTransaction({

const tx = await wallet.sendTransaction({
to,
value,
maxPriorityFeePerGas,
maxFeePerGas,
gasLimit,
nonce,
chainId
gasLimit
});

const result = await ethSendRawTransaction(rawTransaction);

if (result.Consistent?.Ok?.Ok === null) {
res.send('transaction sent');
} else {
res.status(500).send('transaction failed');
}
res.send(`transaction sent with hash: ${tx.hash}`);
}
);

app.post(
'/transfer-from-canister',
async (req: Request<any, any, { to: string; value: string }>, res) => {
if (canisterAddress.value === null) {
canisterAddress.value = ethers.computeAddress(
ethers.hexlify(
await ecdsaPublicKey([ic.id().toUint8Array()])
)
);
}
const wallet = new TEcdsaWallet(
[ic.id().toUint8Array()],
// ethers.getDefaultProvider('sepolia')
ethers.getDefaultProvider('https://sepolia.base.org')
);

const to = req.body.to;
const value = ethers.parseEther(req.body.value);
const maxPriorityFeePerGas = await ethMaxPriorityFeePerGas();
const baseFeePerGas = BigInt(
(await ethFeeHistory()).Consistent?.Ok[0].baseFeePerGas[0]
);
const maxFeePerGas = baseFeePerGas * 2n + maxPriorityFeePerGas;
const gasLimit = 21_000n;
const nonce = await ethGetTransactionCount(canisterAddress.value);

let tx = ethers.Transaction.from({
const tx = await wallet.sendTransaction({
to,
value,
maxPriorityFeePerGas,
maxFeePerGas,
gasLimit,
nonce,
chainId
gasLimit
});

const unsignedSerializedTx = tx.unsignedSerialized;
const unsignedSerializedTxHash =
ethers.keccak256(unsignedSerializedTx);
res.send(`transaction sent with hash: ${tx.hash}`);
}
);

const signedSerializedTxHash = await signWithEcdsa(
[ic.id().toUint8Array()],
ethers.getBytes(unsignedSerializedTxHash)
);
return app.listen();
});

const { r, s, v } = calculateRsvForTEcdsa(
canisterAddress.value,
unsignedSerializedTxHash,
signedSerializedTxHash
);
// TODO should we cach the address?
class TEcdsaWallet extends ethers.AbstractSigner {
derivationPath: Uint8Array[] = [];

tx.signature = {
r,
s,
v
};
constructor(
derivationPath: Uint8Array[],
provider: ethers.Provider | null = null
) {
super(provider);

const rawTransaction = tx.serialized;
this.derivationPath = derivationPath;
}

const result = await ethSendRawTransaction(rawTransaction);
connect(provider: null | ethers.Provider): TEcdsaWallet {
return new TEcdsaWallet(this.derivationPath, provider);
}

if (result.Consistent?.Ok?.Ok === null) {
res.send('transaction sent');
} else {
res.status(500).send('transaction failed');
}
}
);
async getAddress(): Promise<string> {
return ethers.computeAddress(
ethers.hexlify(await ecdsaPublicKey(this.derivationPath))
);
}

return app.listen();
async signTransaction(
txRequest: ethers.TransactionRequest
): Promise<string> {
let tx = ethers.Transaction.from(txRequest);

const unsignedSerializedTx = tx.unsignedSerialized;
const unsignedSerializedTxHash = ethers.keccak256(unsignedSerializedTx);

const signedSerializedTxHash = await signWithEcdsa(
this.derivationPath,
ethers.getBytes(unsignedSerializedTxHash)
);

const { r, s, v } = calculateRsvForTEcdsa(
await this.getAddress(),
unsignedSerializedTxHash,
signedSerializedTxHash
);

tx.signature = {
r,
s,
v
};

const rawTransaction = tx.serialized;

return rawTransaction;
}

async signMessage(message: string | Uint8Array): Promise<string> {
throw new Error(`TEcdsaWallet: signMessage is not implemented`);
}

async signTypedData(
domain: ethers.TypedDataDomain,
types: Record<string, ethers.TypedDataField[]>,
value: Record<string, any>
): Promise<string> {
throw new Error(`TEcdsaWallet: signTypedData is not implemented`);
}
}

// TODO the default provider looks like it does multiple requests/responses!
// TODO it's basically the EVM RPC canister...I am not seeing the need for the EVM RPC canister...

// TODO add tests for different providers??
// TODO test different url configurations etc?
ethers.FetchRequest.registerGetUrl(async (fetchRequest) => {
console.log('we tried to do a request but failed :(');
console.log('fetchRequest.url', fetchRequest.url);
console.log('fetchRequest.method', fetchRequest.method);
console.log('fetchRequest.headers', Object.entries(fetchRequest.headers));
console.log('fetchRequest.body.length', fetchRequest.body?.length);

console.log(JSON.parse(Buffer.from(fetchRequest.body ?? '{}').toString()));

try {
const response = await fetch(fetchRequest.url, {
method: fetchRequest.method,
headers: Object.entries(fetchRequest.headers).map(
([key, value]) => {
return {
name: key,
value
};
}
),
body: fetchRequest.body
});

console.log('response', response);

console.log(await response.text());
console.log(await response.json());

const fetchResponse = new ethers.FetchResponse(
response.status,
'OK',
{}, // TODO figure out headers
new Uint8Array(await response.arrayBuffer()),
fetchRequest
);

console.log('fetchResponse', fetchResponse);

return fetchResponse;
} catch (error) {
console.log(error);

return new ethers.FetchResponse(500, '', {}, null, fetchRequest);
}
});
4 changes: 2 additions & 2 deletions examples/ic_evm_rpc/test/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export function getTests(canisterId: string): Test[] {
const responseText = await response.text();

return {
Ok: responseText === 'transaction sent'
Ok: responseText.startsWith('transaction sent with hash:')
};
}
},
Expand Down Expand Up @@ -144,7 +144,7 @@ export function getTests(canisterId: string): Test[] {
const responseText = await response.text();

return {
Ok: responseText === 'transaction sent'
Ok: responseText.startsWith('transaction sent with hash:')
};
}
},
Expand Down
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"http-message-parser": "^0.0.34",
"js-sha256": "0.9.0",
"net": "^1.0.2",
"pako": "^2.1.0",
"text-encoding": "0.7.0",
"ts-node": "10.3.1",
"typescript": "^5.2.2",
Expand Down
71 changes: 67 additions & 4 deletions src/lib/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { ic, Principal } from './';
import { ic, Principal, processChunkedBody } from './';
import { IDL } from '@dfinity/candid';
import { URL } from 'url';
import * as fs from 'fs';
import { readFile } from 'fs/promises';
const pako = require('pako');

export async function azleFetch(
input: RequestInfo | URL,
Expand Down Expand Up @@ -106,9 +107,13 @@ export async function azleFetch(
}

if (url.protocol === 'https:' || url.protocol === 'http:') {
console.log('azleFetch http 0');

const body = await prepareRequestBody(init);
// TODO also do headers and method and everything like on the client?

console.log('azleFetch http 1');

const response = await azleFetch(`icp://aaaaa-aa/http_request`, {
body: serialize({
args: [
Expand All @@ -125,17 +130,75 @@ export async function azleFetch(
globalThis._azleOutgoingHttpOptionsCycles ?? 3_000_000_000n // TODO this seems to be a conservative max size
})
});

console.log('azleFetch http 2');

const responseJson = await response.json();

console.log('responseJson', responseJson);

console.log('azleFetch http 3');

console.log('responseJson.headers', responseJson.headers);
console.log('responseJson.headers.length', responseJson.headers.length);
console.log('responseJson.headers[0]', responseJson.headers[0]);
console.log(Array.isArray(responseJson.headers));

const bodyIsGZipped =
responseJson.headers.find(({ name, value }) => {
return (
name.toLowerCase() === 'content-encoding' &&
value.toLowerCase() === 'gzip'
);
}) !== undefined;

console.log('bodyIsGZipped', bodyIsGZipped);

console.log('responseJson.body.length', responseJson.body.length);

const unGZippedBody = bodyIsGZipped
? pako.inflate(responseJson.body)
: responseJson.body;

console.log('unGZippedBody.length', unGZippedBody.length);

// TODO do we need to handle a chunked body on the frontend too?
const bodyIsChunked =
responseJson.headers.find(({ name, value }) => {
return (
name.toLowerCase() === 'transfer-encoding' &&
value.toLowerCase() === 'chunked'
);
}) !== undefined;

console.log('bodyIsChunked', bodyIsChunked);

// const bufferedBody = Buffer.from(responseJson.body);

// const processedBody = chunkedBody
// ? processChunkedBody(bufferedBody)
// : bufferedBody;

console.log(Buffer.from(unGZippedBody).toString());

// const processedBody = bodyIsChunked
// ? processChunkedBody(Buffer.from(unGZippedBody))
// : unGZippedBody;

const processedBody = Buffer.from(unGZippedBody);

// TODO can we use the response object from wasmedge-quickjs?
return {
status: Number(responseJson.status),
statusText: '', // TODO not done
arrayBuffer: async () => {
return responseJson.body.buffer;
return processedBody.buffer;
},
json: async () => {
return JSON.parse(Buffer.from(responseJson.body).toString());
return JSON.parse(Buffer.from(processedBody).toString());
},
text: async () => {
return Buffer.from(responseJson.body).toString();
return Buffer.from(processedBody).toString();
}
} as any;
}
Expand Down
Loading

0 comments on commit 34514e6

Please sign in to comment.