交易
everPay 有自己独立的交易格式,所有的 everPay 交易都遵循相同的格式,签署相同的签名规范,存储在 arweave 区块链上,供所有人验证。
Schema
字段 | 描述 |
---|---|
tokenSymbol | 代币名称,AR,ETH,USDT,USDC 等。 |
action |
|
from | 签名交易的当前 everPay 账户 ID。 |
to |
|
amount | 类型为 uint,设置时需要进行 decimals 处理,例如 0.1USDT,此处经过 USDT 的 decimals: 6 处理后,为 100000。
|
fee | 手续费,类型为 uint。需要进行 decimals 处理,例如 0.1USDT,此处经过 USDT 的 decimals: 6 处理后,为 100000。 |
feeRecipient | 手续费收款 everPay 账户 ID,通过 info API 接口获取。 |
nonce | unix milliseconds,unix 毫秒时间戳。 |
tokenID | 通过 info API 接口获取,必须与 tokenSymbol 对应的 token id 字段一致。 |
chainType | chainType 必须与 info API 接口获取的 tokenSymbol 对应的 token chainType 一致。 |
chainID | chainID 必须与 info API 接口获取的 tokenSymbol 对应的 chainID 一致。 |
data | 附加信息,开发者可自定义JSON 数据,经过 JSON.stringify() 处理后传递。通过 data 可自定义实现一些复杂功能,例如 批量转账。 |
version | 交易版本 'v1' 。 |
以太坊账户示例
const everpayTxWithoutSig = {
tokenSymbol: 'usdt',
action: 'transfer',
from: '0x26361130d5d6E798E9319114643AF8c868412859',
to: '5NPqYBdIsIpJzPeYixuz7BEH_W7BEk_mb8HxBD3OHXo',
amount: '5260000',
fee: '0',
feeRecipient: '0x6451eB7f668de69Fb4C943Db72bCF2A73DeeC6B1',
nonce: '1626079771946',
tokenID: '0xd85476c906b5301e8e9eb58d174a6f96b9dfc5ee',
chainType: 'ethereum',
chainID: '42',
data: '{"hello":"world","this":"is everpay"}',
version: 'v1'
}
Arweave 账户示例
const everpayTxWithoutSig = {
"tokenSymbol": "TUSDC",
"action": "transfer",
"from": "5NPqYBdIsIpJzPeYixuz7BEH_W7BEk_mb8HxBD3OHXo",
"to": "0x26361130d5d6E798E9319114643AF8c868412859",
"amount": "1000000",
"fee": "0",
"feeRecipient": "0xfAC49e12F19743FFc3A756294f1bf70C282E25fA",
"nonce": "1708507073627",
"tokenID": "0xf17a50ecc5fe5f476de2da5481cdd0f0ffef7712",
"chainType": "bsc",
"chainID": "97",
"data": "",
"version": "v1"
}
智能账户示例
const everpayTxWithoutSig = {
"tokenSymbol": "TUSDC",
"action": "transfer",
"from": "eidd92c8451f8c5f1e4ab05ad75bfee0acfd5bbe5e3cf2f99e1fad5d4329fb650bc696b",
"to": "0x26361130d5d6E798E9319114643AF8c868412859",
"amount": "100000",
"fee": "0",
"feeRecipient": "0xfAC49e12F19743FFc3A756294f1bf70C282E25fA",
"nonce": "1708507959655",
"tokenID": "0xf17a50ecc5fe5f476de2da5481cdd0f0ffef7712",
"chainType": "bsc",
"chainID": "97",
"data": "",
"version": "v1"
}
messageData
由 Schema 按照统一格式生成,用于:
- 以太坊
personalSign
签名。 - 生成
everHash
。
生成规则
export const getEverpayTxMessageData = (everpayTxWithoutSig: EverpayTxWithoutSig): string => {
const keys = [
'tokenSymbol',
'action',
'from',
'to',
'amount',
'fee',
'feeRecipient',
'nonce',
'tokenID',
'chainType',
'chainID',
'data',
'version'
] as const
return keys.map(key => `${key}:${everpayTxWithoutSig[key]}`).join('\n')
}
其中 EverpayTxWithoutSig
可参考 everpay-js types#EverpayTxWithoutSig
以太坊账户示例
const messageData = `tokenSymbol:usdt
action:transfer
from:0x26361130d5d6E798E9319114643AF8c868412859
to:5NPqYBdIsIpJzPeYixuz7BEH_W7BEk_mb8HxBD3OHXo
amount:5260000
fee:0
feeRecipient:0x6451eB7f668de69Fb4C943Db72bCF2A73DeeC6B1
nonce:1626079771946
tokenID:0xd85476c906b5301e8e9eb58d174a6f96b9dfc5ee
chainType:ethereum
chainID:42
data:{"hello":"world","this":"is everpay"}
version:v1`
Arweave 账户示例
const messageData = `tokenSymbol:TUSDC
action:transfer
from:5NPqYBdIsIpJzPeYixuz7BEH_W7BEk_mb8HxBD3OHXo
to:0x26361130d5d6E798E9319114643AF8c868412859
amount:1000000
fee:0
feeRecipient:0xfAC49e12F19743FFc3A756294f1bf70C282E25fA
nonce:1708507073627
tokenID:0xf17a50ecc5fe5f476de2da5481cdd0f0ffef7712
chainType:bsc
chainID:97
data:
version:v1`
智能账户示例
const messageData = `tokenSymbol:TUSDC
action:transfer
from:eidd92c8451f8c5f1e4ab05ad75bfee0acfd5bbe5e3cf2f99e1fad5d4329fb650bc696b
to:0x26361130d5d6E798E9319114643AF8c868412859
amount:100000
fee:0
feeRecipient:0xfAC49e12F19743FFc3A756294f1bf70C282E25fA
nonce:1708507959655
tokenID:0xf17a50ecc5fe5f476de2da5481cdd0f0ffef7712
chainType:bsc
chainID:97
data:
version:v1`
everHash
每一笔 everPay 交易都有唯一标识的 everHash
。由 messageData
使用以太坊 hashPersonalMessage
生成的 personalMessageHash
,即为 everHash
。
生成规则
// cp from: https://github.com/ethereumjs/ethereumjs-util/blob/ebf40a0fba8b00ba9acae58405bca4415e383a0d/src/signature.ts#L168
const hashPersonalMessage = (message: Buffer): Buffer => {
const prefix = Buffer.from(
`\u0019Ethereum Signed Message:\n${message.length.toString()}`,
'utf-8'
)
return keccak256(Buffer.concat([prefix, message]))
}
const getPersonalMessageHash = (messageData: string): string => {
const personalMsgBuf = hashPersonalMessage(Buffer.from(messageData))
const personalMessageHash = `0x${personalMsgBuf.toString('hex')}`
return personalMessageHash
}
signature
每一笔 everPay 交易,都需要通过 发送者账户的 钱包 或 该智能账户的 webauthn 生物识别 进行签名,everPay 服务器会校验所有签名的有效性。
以太坊账户模型
通过 ethereum personalSign
签名 messageData
得到 signature
通过 ethers.js 生成签名
const everpayTxWithoutSig = {
tokenSymbol: 'usdt',
action: 'transfer',
from: '0x26361130d5d6E798E9319114643AF8c868412859',
to: '5NPqYBdIsIpJzPeYixuz7BEH_W7BEk_mb8HxBD3OHXo',
amount: '5260000',
fee: '0',
feeRecipient: '0x6451eB7f668de69Fb4C943Db72bCF2A73DeeC6B1',
nonce: '1626079771946',
tokenID: '0xd85476c906b5301e8e9eb58d174a6f96b9dfc5ee',
chainType: 'ethereum',
chainID: '42',
data: '{"hello":"world","this":"is everpay"}',
version: 'v1'
}
// const messageData = getEverpayTxMessageData(everpayTxWithoutSig)
const messageData = `tokenSymbol:usdt
action:transfer
from:0x26361130d5d6E798E9319114643AF8c868412859
to:5NPqYBdIsIpJzPeYixuz7BEH_W7BEk_mb8HxBD3OHXo
amount:5260000
fee:0
feeRecipient:0x6451eB7f668de69Fb4C943Db72bCF2A73DeeC6B1
nonce:1626079771946
tokenID:0xd85476c906b5301e8e9eb58d174a6f96b9dfc5ee
chainType:ethereum
chainID:42
data:{"hello":"world","this":"is everpay"}
version:v1`
// personalSign
const signMessageAsync = async (ethConnectedSigner: Signer, message: string): Promise<string> => {
return await ethConnectedSigner.signMessage(message)
}
const signature = await signMessageAsync(ethConnectedSigner, messageData)
伪代码参考来源:everpay-js src/lib/sign.ts
Arweave 账户模型
由 messageData
使用 sha256
生成的 messageDataHash
。通过 arweave RSA-PSS sha256 签名 messageDataHash
对应的 Uint8Array
(或者 Buffer
),得到的签名结果,再通过 Arweave.utils.bufferTob64Url
(与其他 base64 转换函数有差异) 进行 base64
转换,拼接上 ,{{arOwner}}
后,得到 signature
。
通过 arweave.js 生成签名
const everpayTxWithoutSig = {
"tokenSymbol": "TUSDC",
"action": "transfer",
"from": "5NPqYBdIsIpJzPeYixuz7BEH_W7BEk_mb8HxBD3OHXo",
"to": "0x26361130d5d6E798E9319114643AF8c868412859",
"amount": "1000000",
"fee": "0",
"feeRecipient": "0xfAC49e12F19743FFc3A756294f1bf70C282E25fA",
"nonce": "1708507073627",
"tokenID": "0xf17a50ecc5fe5f476de2da5481cdd0f0ffef7712",
"chainType": "bsc",
"chainID": "97",
"data": "",
"version": "v1"
}
// const messageData = getEverpayTxMessageData(everpayTxWithoutSig)
const messageData = `tokenSymbol:TUSDC
action:transfer
from:5NPqYBdIsIpJzPeYixuz7BEH_W7BEk_mb8HxBD3OHXo
to:0x26361130d5d6E798E9319114643AF8c868412859
amount:1000000
fee:0
feeRecipient:0xfAC49e12F19743FFc3A756294f1bf70C282E25fA
nonce:1708507073627
tokenID:0xf17a50ecc5fe5f476de2da5481cdd0f0ffef7712
chainType:bsc
chainID:97
data:
version:v1`
const signMessageAsync = async (arJWK: ArJWK, messageData: string): Promise<string> => {
const arweave = Arweave.init(options)
const msgDataBuffer = Buffer.from(messageData, 'utf-8')
let arOwner = ''
let signatureB64url = ''
// web
if (arJWK === 'use_wallet') {
try {
await checkArPermissions('ACCESS_PUBLIC_KEY')
} catch {
throw new Error(ERRORS.ACCESS_PUBLIC_KEY_PERMISSION_NEEDED)
}
try {
arOwner = await (window.arweaveWallet).getActivePublicKey()
} catch {
throw new Error(ERRORS.ACCESS_PUBLIC_KEY_FAILED)
}
try {
await checkArPermissions('SIGNATURE')
} catch {
throw new Error(ERRORS.SIGNATURE_PERMISSION_NEEDED)
}
const algorithm = {
name: 'RSA-PSS',
saltLength: 32
}
if ((window.arweaveWallet as any).signMessage !== undefined) {
try {
const signature = await (window.arweaveWallet as any).signMessage(
msgDataBuffer,
{ hashAlgorithm: 'SHA-256' }
)
const buf = new Uint8Array(Object.values(signature))
signatureB64url = Arweave.utils.bufferTob64Url(buf)
} catch {
throw new Error(ERRORS.SIGNATURE_FAILED)
}
} else {
try {
const hash = sha256(messageData)
const signature = await (window.arweaveWallet).signature(
hexToUint8Array(hash.toString()),
algorithm
)
const buf = new Uint8Array(Object.values(signature))
signatureB64url = Arweave.utils.bufferTob64Url(buf)
} catch {
throw new Error(ERRORS.SIGNATURE_FAILED)
}
}
// node
} else {
const hash = sha256(messageData)
const buf = await arweave.crypto.sign(arJWK, hexToUint8Array(hash.toString()), {
saltLength: 32
})
arOwner = arJWK.n
signatureB64url = Arweave.utils.bufferTob64Url(buf)
}
return `${signatureB64url},${arOwner}`
}
const signature = await signMessageAsync(config.arJWK as ArJWK, messageData)
伪代码参考来源:everpay-js src/lib/sign.ts
智能账户模型
const everpayTxWithoutSig = {
"tokenSymbol": "TUSDC",
"action": "transfer",
"from": "eidd92c8451f8c5f1e4ab05ad75bfee0acfd5bbe5e3cf2f99e1fad5d4329fb650bc696b",
"to": "0x26361130d5d6E798E9319114643AF8c868412859",
"amount": "100000",
"fee": "0",
"feeRecipient": "0xfAC49e12F19743FFc3A756294f1bf70C282E25fA",
"nonce": "1708507959655",
"tokenID": "0xf17a50ecc5fe5f476de2da5481cdd0f0ffef7712",
"chainType": "bsc",
"chainID": "97",
"data": "",
"version": "v1"
}
// const messageData = getEverpayTxMessageData(everpayTxWithoutSig)
const messageData = `tokenSymbol:TUSDC
action:transfer
from:eidd92c8451f8c5f1e4ab05ad75bfee0acfd5bbe5e3cf2f99e1fad5d4329fb650bc696b
to:0x26361130d5d6E798E9319114643AF8c868412859
amount:100000
fee:0
feeRecipient:0xfAC49e12F19743FFc3A756294f1bf70C282E25fA
nonce:1708507959655
tokenID:0xf17a50ecc5fe5f476de2da5481cdd0f0ffef7712
chainType:bsc
chainID:97
data:
version:v1`
const hashPersonalMessage = (message: Buffer): Buffer => {
const prefix = Buffer.from(
`\u0019Ethereum Signed Message:\n${message.length.toString()}`,
'utf-8'
)
return keccak256(Buffer.concat([prefix, message]))
}
const getPersonalMessageHash = (messageData: string): string => {
const personalMsgBuf = hashPersonalMessage(Buffer.from(messageData))
const personalMessageHash = `0x${personalMsgBuf.toString('hex')}`
return personalMessageHash
}
const signMessageAsync = async (debug: boolean, isSmartAccount: boolean, email: string, everHash: string, accountData?: any): Promise<string> => {
if (accountData == null) {
const everpayHost = getEverpayHost(debug)
const everId = genEverId(email)
accountData = await getAccountData(everpayHost, everId)
}
const arr = Object.entries(accountData.publicValues) as any
const publicKeyData = {
allowCredentials: arr.map((publicIdValueArr: any) => {
const id = publicIdValueArr[0]
return {
type: 'public-key',
id: Arweave.utils.b64UrlToBuffer(id),
transports: [
'internal',
'usb',
'nfc',
'ble'
].concat(!isMobile ? ['hybrid'] : [])
}
})
}
const assertion = await navigator.credentials.get({
publicKey: {
...publicKeyData,
timeout: 300000,
userVerification: 'required',
challenge: Arweave.utils.b64UrlToBuffer(window.btoa(everHash)),
rpId: getRpId()
} as any
}) as any
if (assertion === null) {
throw new Error('cancelled')
}
const authenticatorData = assertion.response.authenticatorData
const clientDataJSON = assertion.response.clientDataJSON
const rawId = assertion.rawId
const signature = assertion.response.signature
const userHandle = assertion.response.userHandle
const sigJson = {
id: assertion?.id,
rawId: Arweave.utils.bufferTob64Url(rawId),
clientDataJSON: Arweave.utils.bufferTob64Url(clientDataJSON),
authenticatorData: Arweave.utils.bufferTob64Url(authenticatorData),
signature: Arweave.utils.bufferTob64Url(signature),
userHandle: Arweave.utils.bufferTob64Url(userHandle)
}
const sig = window.btoa(JSON.stringify(sigJson))
return `${sig},${accountData.publicValues[assertion?.id]},FIDO2`
}
const signature = await signMessageAsync(true, true, '[email protected]', getPersonalMessageHash(messageData))
- 用于 以太坊 personalSign 签名的是
messageData
string,得到的结果即为signature
- 用于 arweave RSA-PSS sha256 签名的是
messageDataHash
Buffer,得到的结果需要进一步通过Arweave.utils.bufferTob64Url
转换得到的base64 string
,并拼接上,{{arOwner}}
才是signature
。 - 用于 智能账户 webauthn 的签名
- 将
messageData
使用getPersonalMessageHash
得到everHash
- 通过后端接口获取该邮箱账户对应的 eid 相对应的 公钥信息
- 使用对应的公钥信息,调用 webauthn 对
everHash
进行签名 - 组装最终的签名结果
- 将
signature 校验
每一笔 everPay 交易,都需要通过 发送者账户的 钱包 或 该智能账户的 webauthn 生物识别 进行签名后,将签名 everPay 交易一起提交 everPay 服务器,everPay 服务器会校验所有签名,来确保交易的有效性。
以太坊账户模型
const signature = await signMessageAsync(ethConnectedSigner, messageData)
const verified = ethers.utils.verifyMessage(messageData, signature).toLowerCase() === everpayTxWithoutSig.from.toLowerCase()
Arweave 账户模型
const signature = await signMessageAsync(config.arJWK as ArJWK, message)
const hash = sha256(messageData)
// arOwner 为 arweave 钱包 publicKey
const [sigB64url, arOwner] = signature.split(',')
const verified = arweave.crypto.verify(
arOwner,
hexToUint8Array(hash.toString()),
Arweave.utils.b64UrlToBuffer(sigB64url)
)
智能账户模型
提交交易
将 everPay 交易通过 POST 请求提交 everPay 后端服务器 tx
接口。
字段
除 Schema
定义字段外,补充签名得到signature
作为 sig
字段。
补充字段 | 描述 |
---|---|
sig | 根据不同账户模型签名,生成的 signature 。 |
示例
{
tokenSymbol: 'ar',
action: 'transfer',
from: '5NPqYBdIsIpJzPeYixuz7BEH_W7BEk_mb8HxBD3OHXo',
to: '0x26361130d5d6E798E9319114643AF8c868412859',
amount: '100',
fee: '0',
feeRecipient: '0x6451eB7f668de69Fb4C943Db72bCF2A73DeeC6B1',
nonce: '1629276767583',
tokenID: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA,0xcc9141efa8c20c7df0778748255b1487957811be',
chainType: 'arweave,ethereum',
chainID: '0,42',
data: '{"hello":"world","this":"is everpay"}',
version: 'v1',
sig: 'Lg8Xgk_LZn_H-HVOz042wbhv5RVQjc7Z0iVV4_UbWWgoqHnboB6PQujtCtu1_QW0cXPqakm9sLi7fJlhK7Hm7UMFQiwXbVB_bClr73GKAcV0tpWye9BUsKw9SfOnFAHCCufF4C1PPt4xRrJp5UeG-smonQ9k4t0GmoXnXoSfmFxsEvaId5SeNaOZa1JYMzReo8-P4m5EdrTKLNgWwo28OOi4GbpXIzRxorJp-dwhsNhQHu4vzOq4rflGRwQKb9bj4S92YqEp2wXRRU7ebEiBJlGjQrf0HgTr7gZO_q3gI5FQgsL_UbOo4sp5hL69IUOfRxmr_RTiLZZzQRu-0dJBsWOSWYC5232fRf3MwogIELdDUl3dVCz5PDnXp8AOPKBQCiblu74oTSyKhsVMvwfER125dXyKtxJLlxTkDhEOPzTJdufy-Czs1pE_ZPKj4z44P3W7UdGiAt9rXYQb6JjMNOpG1_S7RMa5OKoCV4MbuK2CGFCNBE0h4zxeXZfXLOMSImrfFZ4nZAHkTbpKgpH1hPDEMGsEEgwvDl6_AyjrMOebAAyJGj6keyy9tf7lQBKKaj2-bGG6PIVC-l7wCXJizFt-3Cb0aC6ZXaCHGdhHuI-7Ime4M5iZESiBhkIhtOV3ADjqks174o0J0zKxE0NVl14tuu5tY-UfJ6kAkqbujLs,odtNk97a4PARR0I8g3kQpzlFVmPg-udyjfl81fbTioyP2pEw5tP5A1-FVqR-QFFPskW-j7yAze5usYNWHEir7oVQ9d9bbkcZIDEPqwSTO1JoD1BKXeeBK0xsmiSgxeY7uuRXWdhXREhlmIMsV8ObakEeXdbbxbs89XaZHBuES7boASrRVDXRz_mhMu6u_58OdLeMwR3I1BCH6nphNGVOehA7GOOqEBvtesBset0bNaLCb0JpSg5ZW_0AGLP-XydzE3IPLLx4NQEEJY21y8fChxYM4jntI78l5hojp9NlmS69EXlj0PoMjsbaWaz9WtnZaMAbnaOGAHhv8Y_TNmBI0FHpqHaGPP906Mnrgdm3tl2L40EX-Q6-liNVkB56CmPxXzSesu-4x5LLYxQ-aX3W6Hj7RCDTacxqUJHzOrhJqXSx6Jx0t8CwyfReMgVv4p5t1C3OZ8yYbJ_H3LdkeriVniaC5jQdMyIJ6QBMzr1XdXIw9WuEG2kCIYtvOp2qDuu9o2SY-9W4Yv7VWRDfWO38xxR4ZO65MMAdZxeaZ4w8sK_owH46Wm0XoT3Al-LPypaeijWqlHEu4R8c2ersD3xkDvXC_lNtaQw_qyfI3UEH5fWupY4zhZeDGkvXQh32Fv4CxlZL58iUHv9SvR7p5LgBCC3AVUbn7Sqc4xPUCZMj-Tc'
}
nonce
nonce
为用户在客户端生成,everPay 服务器可接收与服务器时间上下 100s 的误差。- 用户每次提交 everPay 的交易,
nonce
必须比该用户上一笔nonce
数值大。
交易记录
提交至 everPay 服务器,经过签名校验通过的 everPay 交易,可以通过 everPay 接口获取交易记录。
everPay 在 Schema
定义字段、sig
签名字段外,添加了部分字段,用于 everPay 业务。
字段
补充字段 | 描述 |
---|---|
everHash | 每笔 everPay 交易都对应一个唯一的 everHash ,everHash 生成参考 everHash。 |
timestamp |
|
status |
|
internalStatus | 批量转账增加的字段,仅在批量转账的内部交易失败时,返回具体错误信息。批量转账内部交易成功、转账、提现、充值时,值都为 success 。 |
id |
|
targetChainTxHash |
|
查询接口
- txs 查询所有 everPay 交易记录。
- txsByAccount 查询具体 everPay 账户的交易记录。
- txByHash 根据
everHash
查询 everPay 交易记录。 - mintedTxByChainTxHash 根据充值的区块链记录ID (如以太坊为
txHash
),查询充值的 everPay 交易记录。