-
Notifications
You must be signed in to change notification settings - Fork 376
[WIP] Add automated preparation of Ethereum device app for plugins and ERC20 #587
Changes from 9 commits
166191b
38c72de
cc9a04d
01d0107
71ba9dc
53100ee
6c6daa8
aeba097
1b02a4e
7a696dc
1241f1e
e5617ac
a6bdca5
4547ead
cdd0d27
e348025
68dddc8
9413041
97c291d
12cbdd0
3f1b39d
1b7cd40
8a9a4f2
1f2c4fd
034ad45
e79b7fc
138ed82
71bb241
148e00a
f036341
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,6 +22,9 @@ import { EthAppPleaseEnableContractData } from "@ledgerhq/errors"; | |
import type Transport from "@ledgerhq/hw-transport"; | ||
import { BigNumber } from "bignumber.js"; | ||
import { encode, decode } from "rlp"; | ||
import { ethers } from "ethers"; | ||
import { byContractAddress } from "./erc20"; | ||
import { getPluginForContractMethod } from "./plugins"; | ||
|
||
export type StarkQuantizationType = | ||
| "eth" | ||
|
@@ -87,6 +90,7 @@ export default class Eth { | |
"starkUnsafeSign", | ||
"eth2GetPublicKey", | ||
"eth2SetWithdrawalIndex", | ||
"setExternalPlugin", | ||
], | ||
scrambleKey | ||
); | ||
|
@@ -167,17 +171,7 @@ export default class Eth { | |
* const signed = await appEth.signTransaction(path, rawTxHex) | ||
*/ | ||
provideERC20TokenInformation({ data }: { data: Buffer }): Promise<boolean> { | ||
return this.transport.send(0xe0, 0x0a, 0x00, 0x00, data).then( | ||
() => true, | ||
(e) => { | ||
if (e && e.statusCode === 0x6d00) { | ||
// this case happen for older version of ETH app, since older app version had the ERC20 data hardcoded, it's fine to assume it worked. | ||
// we return a flag to know if the call was effective or not | ||
return false; | ||
} | ||
throw e; | ||
} | ||
); | ||
return _provideERC20TokenInformation.call(this, { data }); | ||
} | ||
|
||
/** | ||
|
@@ -240,6 +234,35 @@ export default class Eth { | |
toSend.push(buffer); | ||
offset += chunkSize; | ||
} | ||
|
||
rlpTx = ethers.utils.RLP.decode("0x" + rawTxHex); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this makes signTransactionLargeChainID2 test fail with "data array too short". There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also just before this part, we modify rawTx in case its an EIP155 transaction. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no idea for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. rawTx is actually not updated so its ok There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. signTransactionLargeChainID2 fixed |
||
|
||
const decodedTx = { | ||
data: rlpTx[5], | ||
to: rlpTx[3], | ||
}; | ||
|
||
if (decodedTx.data.length >= 10) { | ||
const erc20Info = byContractAddress(decodedTx.to); | ||
if (erc20Info) { | ||
_provideERC20TokenInformation.call(this, erc20Info); | ||
} | ||
|
||
const selector = decodedTx.data.substring(0, 10); | ||
const plugin = getPluginForContractMethod(decodedTx.to, selector); | ||
|
||
if (plugin) { | ||
const { payload, signature /*, erc20OfInterest, abi*/ } = plugin; | ||
|
||
// TODO | ||
// use abi and erc20OfInterest to call _provideERC20TokenInformation | ||
// with ethers | ||
// | ||
|
||
_setExternalPlugin.call(this, payload, signature); | ||
} | ||
} | ||
|
||
return foreach(toSend, (data, i) => | ||
this.transport | ||
.send(0xe0, 0x04, i === 0 ? 0x00 : 0x80, 0x00, data) | ||
|
@@ -1002,4 +1025,63 @@ eth.signPersonalMessage("44'/60'/0'/0/0", Buffer.from("test").toString("hex")).t | |
} | ||
); | ||
} | ||
|
||
/** | ||
* Set the name of the plugin that should be used to parse the next transaction | ||
* | ||
* @param pluginName string containing the name of the plugin, must have length between 1 and 30 bytes | ||
* @return True if the method was executed successfully | ||
*/ | ||
setExternalPlugin( | ||
pluginName: string, | ||
contractAddress: string, | ||
selector: string | ||
): Promise<boolean> { | ||
return _setExternalPlugin.call(this, pluginName, contractAddress, selector); | ||
} | ||
} | ||
|
||
// PRIVATE | ||
|
||
function _provideERC20TokenInformation({ | ||
data, | ||
}: { | ||
data: Buffer, | ||
}): Promise<boolean> { | ||
return this.transport.send(0xe0, 0x0a, 0x00, 0x00, data).then( | ||
() => true, | ||
(e) => { | ||
if (e && e.statusCode === 0x6d00) { | ||
// this case happen for older version of ETH app, since older app version had the ERC20 data hardcoded, it's fine to assume it worked. | ||
// we return a flag to know if the call was effective or not | ||
return false; | ||
} | ||
throw e; | ||
} | ||
); | ||
} | ||
|
||
function _setExternalPlugin( | ||
payload: string, | ||
signature: string | ||
): Promise<boolean> { | ||
let payloadBuffer = Buffer.from(payload, "hex"); | ||
let signatureBuffer = Buffer.from(signature, "hex"); | ||
let buffer = Buffer.concat([payloadBuffer, signatureBuffer]); | ||
return this.transport.send(0xe0, 0x12, 0x00, 0x00, buffer).then( | ||
() => true, | ||
(e) => { | ||
if (e && e.statusCode === 0x6a80) { | ||
// this case happen when the plugin name is too short or too long | ||
return false; | ||
} else if (e && e.statusCode === 0x6984) { | ||
// this case happen when the plugin requested is not installed on the device | ||
return false; | ||
} else if (e && e.statusCode === 0x6d00) { | ||
// this case happen for older version of ETH app | ||
return false; | ||
} | ||
throw e; | ||
} | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import data from "@ledgerhq/cryptoassets/data/dapps/ethereum"; | ||
|
||
/** | ||
* Retrieve the name of a plugin compatible with a given contract address and a method selector | ||
*/ | ||
|
||
export const getPluginForContractMethod = (contractAddress, selector) => { | ||
if (contractAddress in data) { | ||
let contractSelectors = data[contractAddress]; | ||
|
||
if (selector in contractSelectors) { | ||
return { | ||
payload: contractSelectors[selector]["serialized_data"], | ||
signature: contractSelectors[selector]["signature"], | ||
erc20OfInterest: contractSelectors[selector]["erc20OfInterest"], | ||
abi: contractSelectors[selector]["abi"], | ||
}; | ||
} | ||
} | ||
}; | ||
|
||
exports.getPluginForContractMethod = getPluginForContractMethod; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
const path = require("path"); | ||
const { readFileJSON } = require("../utils"); | ||
|
||
const mapObject = (obj, fn) => Object.fromEntries(Object.entries(obj).map(fn)); | ||
|
||
module.exports = { | ||
paths: ["dapps/ethereum"], | ||
output: "data/dapps/ethereum.js", | ||
outputTemplate: (data) => | ||
"module.exports = " + | ||
JSON.stringify( | ||
data.reduce( | ||
(acc, obj) => ({ | ||
...acc, | ||
...obj, | ||
}), | ||
{} | ||
) | ||
), | ||
|
||
loader: async ({ folder, id }) => { | ||
const [signatures, bare] = await Promise.all([ | ||
readFileJSON(path.join(folder, id, "b2c_signatures.json")), | ||
readFileJSON(path.join(folder, id, "b2c.json")), | ||
]); | ||
|
||
const addresses = Object.keys(signatures); | ||
|
||
const abisList = await Promise.all( | ||
addresses.map((address) => | ||
readFileJSON( | ||
path.join(folder, id, "abis", `${address}.abi.json`) | ||
).catch(() => {}) | ||
) | ||
); | ||
|
||
const abis = addresses.reduce( | ||
(acc, address, i) => ({ | ||
...acc, | ||
[address]: abisList[i], | ||
}), | ||
{} | ||
); | ||
|
||
return bare.contracts.reduce( | ||
(acc, contract) => ({ | ||
...acc, | ||
[contract.address]: mapObject( | ||
contract.selectors, | ||
([selector, data]) => [ | ||
selector, | ||
{ | ||
...signatures[contract.address][selector], | ||
erc20OfInterest: data.erc20OfInterest, | ||
abi: abis[contract.address], | ||
}, | ||
] | ||
), | ||
}), | ||
{} | ||
); | ||
}, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we switch to ethers, we should drop rlp and use ethers.utils.RLP.
However I tried and it's not a dropin replacement. Shoud be similar though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done