Skip to content

Commit

Permalink
rework payment key support
Browse files Browse the repository at this point in the history
  • Loading branch information
SebastienGllmt committed Oct 18, 2020
1 parent 5ae537e commit 9051356
Show file tree
Hide file tree
Showing 9 changed files with 212 additions and 133 deletions.
7 changes: 5 additions & 2 deletions README.md
Expand Up @@ -62,6 +62,7 @@ npm run test

```js
{
// bech32 address for strict match. hex-encoded address for matching on the payment key
addresses: Array<string>
}
```
Expand Down Expand Up @@ -205,6 +206,7 @@ npm run test

```js
{
// bech32 address for strict match. hex-encoded address for matching on the payment key
addresses: Array<string>
}
```
Expand Down Expand Up @@ -238,8 +240,9 @@ npm run test
{
// addresses may contain several different things.
// 1. For reward addresses, this field accepts the hex (as a string)
// 2. For Ptr / enterprise / base addresses, this field will accept the hex of the _payment key_ as a string.
// 3. For Byone, use the Ae2/Dd address.
// 2. Bech32 addresses will strictly match the address passed in
// 3. hex-encoded addresses will match on any address with the same payment key
// 4. For Byron, use the Ae2/Dd address.
addresses: Array<string>,
// omitting "after" means you query starting from the genesis block
after?: {
Expand Down
98 changes: 87 additions & 11 deletions src/services/filterUsedAddress.ts
@@ -1,8 +1,12 @@
import { Pool } from "pg";
import { Request, Response } from "express";
import {
Address,
ByronAddress,
} from "@emurgo/cardano-serialization-lib-nodejs";

import config from "config";
import { assertNever, getCardanoSpendingKeyHash, isHex, validateAddressesReq } from "../utils";
import { assertNever, validateAddressesReq, getSpendingKeyHash } from "../utils";

const baseQuery = `
select ( select json_agg((address))
Expand All @@ -27,46 +31,118 @@ const filterByPaymentCredQuery = `

const addressesRequestLimit:number = config.get("server.addressRequestLimit");

export function getAddressesByType(addresses: string[]): {
/**
* note: we keep track of explicit bech32 addresses
* since it's possible somebody wants the tx history for a specific address
* and not the tx history for the payment key of the address
*/
legacyAddr: string[],
bech32: string[],
paymentKeyMap: Map<string, Set<string>>,
} {
const legacyAddr = [];
const bech32 = [];
const paymentKeyMap = new Map();
for (const address of addresses) {
// 1) Check if it's a Byron-era address
if (ByronAddress.is_valid(address)) {
legacyAddr.push(address);
continue;
}
// 2) check if it's a valid bech32 address
try {
const wasmBech32 = Address.from_bech32(address);
bech32.push(address);
wasmBech32.free();
continue;
} catch (_e) {
// silently discard any non-valid Cardano addresses
}
try {
const wasmAddr = Address.from_bytes(
Buffer.from(address, "hex")
);
const spendingKeyHash = getSpendingKeyHash(wasmAddr);
if (spendingKeyHash != null) {
const addressesForKey = paymentKeyMap.get(spendingKeyHash) ?? new Set();
addressesForKey.add(address);
paymentKeyMap.set(spendingKeyHash, addressesForKey);
}
wasmAddr.free();
continue;
} catch (_e) {
// silently discard any non-valid Cardano addresses
}
}

return {
legacyAddr,
bech32,
paymentKeyMap,
};
}


export const filterUsedAddresses = (pool : Pool) => async (req: Request, res: Response) => {
if(!req.body || !req.body.addresses) {
throw new Error("no addresses in request body.");
return;
}
const verifiedAddrs = validateAddressesReq(addressesRequestLimit, req.body.addresses);
const addressTypes = getAddressesByType(req.body.addresses);
switch(verifiedAddrs.kind){
case "ok": {
const paymentCreds = new Set(verifiedAddrs.value.filter(isHex).map((s:string) => `\\x${s}`));
const addresses = new Set(verifiedAddrs.value.filter(addr => !isHex(addr)));
const regularAddresses = [
...addressTypes.legacyAddr,
...addressTypes.bech32,
];

const result: Set<string> = new Set();

if (paymentCreds.size > 0) {
if (addressTypes.paymentKeyMap.size > 0) {
// 1) Get all transactions that contain one of these payment keys
const queryResult = await pool.query(filterByPaymentCredQuery, [Array.from(paymentCreds)]);
const queryResult = await pool.query(
filterByPaymentCredQuery,
[Array
.from(addressTypes.paymentKeyMap.keys())
.map(addr => `\\x${addr}`)
]
);
// 2) get all the addresses inside these transactions
const addressesInTxs = queryResult.rows.flatMap( tx => [tx.inputs, tx.outputs]).flat();
console.log(queryResult.rows);
console.log(Array.from(addressTypes.paymentKeyMap.keys()));
// 3) get the payment credential for each address in the transaction
const keysInTxs: Array<string> = addressesInTxs.reduce(
(arr, next) => {
const paymentCred = getCardanoSpendingKeyHash(next);
if (paymentCred != null) arr.push(paymentCred);
try {
const wasmAddr = Address.from_bech32(next);
const paymentCred = getSpendingKeyHash(wasmAddr);
if (paymentCred != null) arr.push(paymentCred);
wasmAddr.free();
} catch (_e) {
// silently discard any non-valid Cardano addresses
}

return arr;
},
([] as Array<string>)
);
// 4) filter addresses to the ones we care about for this filterUsed query
keysInTxs
.filter(addr => paymentCreds.has(`\\x${addr}`))
.flatMap(addr => Array.from(addressTypes.paymentKeyMap.get(addr) ?? []))
.forEach(addr => result.add(addr));
}
if (addresses.size > 0) {
if (regularAddresses.length > 0) {
// 1) Get all transactions that contain one of these addresses
const queryResult = await pool.query(filterByAddressQuery, [Array.from(addresses)]);
const queryResult = await pool.query(filterByAddressQuery, [regularAddresses]);
// 2) get all the addresses inside these transactions
const addressesInTxs = queryResult.rows.flatMap( tx => [tx.inputs, tx.outputs]).flat();
// 3) filter addresses to the ones we care about for this filterUsed query
const addressSet = new Set(regularAddresses);
addressesInTxs
.filter(addr => addresses.has(addr))
.filter(addr => addressSet.has(addr))
.forEach(addr => result.add(addr));
}
res.send([...result]);
Expand Down
63 changes: 1 addition & 62 deletions src/services/transactionHistory.ts
@@ -1,13 +1,10 @@
import axios from "axios";

import { contentTypeHeaders, errMsgs, graphqlEndpoint, UtilEither} from "../utils";
import { contentTypeHeaders, errMsgs, graphqlEndpoint, UtilEither, getAddressesByType } from "../utils";

import { rowToCertificate, BlockEra, BlockFrag, Certificate, TransInputFrag, TransOutputFrag, TransactionFrag} from "../Transactions/types";

import { Pool } from "pg";
import { ByronAddress, Address } from "@emurgo/cardano-serialization-lib-nodejs";



/**
Everything else in this repo is using graphql, so why psql here?
Expand Down Expand Up @@ -207,64 +204,6 @@ const askTransactionSqlQuery = `

const MAX_INT = "2147483647";

// ex: TODO example
const PAYMENT_KEY_HEX_LENGTH = 56;

// ex: e19842145a1693dfbf809963c7a605b463dce5ca6b66820341a443501e
const STAKING_ADDR_HEX_LENGTH = 58;

const HEX_REGEXP = RegExp("^[0-9a-fA-F]+$");

function getAddressesByType(addresses: string[]): {
/**
* note: we keep track of explicit bech32 addresses
* since it's possible somebody wants the tx history for a specific address
* and not the tx history for the payment key of the address
*/
legacyAddr: string[],
bech32: string[],
paymentCreds: string[],
stakingKeys: string[],
} {
const legacyAddr = [];
const bech32 = [];
const paymentCreds = [];
const stakingKeys = [];
for (const address of addresses) {
// 1) Check if it's a Byron-era address
if (ByronAddress.is_valid(address)) {
legacyAddr.push(address);
continue;
}
// 2) check if it's a valid bech32 address
try {
const wasmBech32 = Address.from_bech32(address);
bech32.push(address);
wasmBech32.free();
continue;
} catch (_e) {
// silently discard any non-valid Cardano addresses
}
// 3) check if it's a payment key
if (address.length === PAYMENT_KEY_HEX_LENGTH && HEX_REGEXP.test(address)) {
paymentCreds.push(`\\x${address}`);
continue;
}
// 4) check if it's a staking key
if (address.length === STAKING_ADDR_HEX_LENGTH && HEX_REGEXP.test(address)) {
stakingKeys.push(`\\x${address}`);
continue;
}
}

return {
legacyAddr,
bech32,
paymentCreds,
stakingKeys,
};
}

export const askTransactionHistory = async (
pool: Pool
, limit: number
Expand Down
19 changes: 12 additions & 7 deletions src/services/utxoForAddress.ts
Expand Up @@ -2,7 +2,7 @@ import { Pool } from "pg";
import { Request, Response } from "express";

import config from "config";
import { assertNever, isHex, validateAddressesReq } from "../utils";
import { assertNever, validateAddressesReq, getAddressesByType, } from "../utils";

const utxoForAddressQuery = `
select tx_out.address
Expand Down Expand Up @@ -30,12 +30,21 @@ export const utxoForAddresses = (pool: Pool) => async (req: Request, res: Respon
throw new Error("error, no addresses.");
return;
}
const addressTypes = getAddressesByType(req.body.addresses);
const verifiedAddresses = validateAddressesReq(addressesRequestLimit
, req.body.addresses);
switch(verifiedAddresses.kind){
case "ok": {
const paymentCreds = verifiedAddresses.value.filter(isHex).map((s:string) => `\\x${s}`);
const result = await pool.query(utxoForAddressQuery, [verifiedAddresses.value, paymentCreds]);
const result = await pool.query(
utxoForAddressQuery,
[
[
...addressTypes.legacyAddr,
...addressTypes.bech32,
]
, addressTypes.paymentCreds
]
);
const utxos = result.rows.map ( utxo =>
({ utxo_id: `${utxo.hash}:${utxo.index}`
, tx_hash: utxo.hash
Expand All @@ -52,7 +61,3 @@ export const utxoForAddresses = (pool: Pool) => async (req: Request, res: Respon
default: return assertNever(verifiedAddresses);
}
};




3 changes: 2 additions & 1 deletion src/services/utxoSumForAddress.ts
Expand Up @@ -3,6 +3,7 @@ import axios from "axios";
import { contentTypeHeaders, graphqlEndpoint, UtilEither} from "../utils";

export const askUtxoSumForAddresses = async (addresses: string[]): Promise<UtilEither<string>> => {
// TODO: support for payment keys
const query = `
query UtxoForAddresses($addresses: [String]) {
utxos_aggregate(where: {
Expand All @@ -28,7 +29,7 @@ export const askUtxoSumForAddresses = async (addresses: string[]): Promise<UtilE
&& "value" in ret.data.data.utxos_aggregate.aggregate.sum)
return { kind: "ok", value: ret.data.data.utxos_aggregate.aggregate.sum.value };
else
return { kind: "error", errMsg: "utxoSumforAddresses, could not unstand graphql response."};
return { kind: "error", errMsg: "utxoSumforAddresses, could not understand graphql response."};


};

0 comments on commit 9051356

Please sign in to comment.