Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: improve string type safety #145

Merged
merged 21 commits into from
Nov 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
bd430f6
feat(core): add utils for custom string types, safer pool id types
mkazlauskas Nov 23, 2021
91c0f59
refactor: add ProviderFailure.InvalidResponse, use improved PoolId type
mkazlauskas Nov 23, 2021
bd6506d
refactor(core): convert Cardano/util and Cardano/StakePool into dirs
mkazlauskas Nov 24, 2021
eeb2074
refactor: improve Cardano.Address and Cardano.RewardAccount types
mkazlauskas Nov 24, 2021
90a1544
refactor: improve Cardano.TransactionId type
mkazlauskas Nov 24, 2021
b24a899
refactor: add Cardano.BlockId type
mkazlauskas Nov 24, 2021
bc8953d
refactor: add Ed25519Signature and Ed25519PublicKey types
mkazlauskas Nov 24, 2021
9ee9070
refactor: update StakePool.rewardAccount to Cardano.RewardAccount
mkazlauskas Nov 24, 2021
98ce920
refactor: convert Hash16 to a validated type, add Ed25519KeyHash and …
mkazlauskas Nov 25, 2021
3f3dad6
refactor: change StakePool.owners to be Cardano.RewardAccount[]
mkazlauskas Nov 25, 2021
b2c3048
refactor: split Hash16 into 2 more concrete types
mkazlauskas Nov 25, 2021
28de368
refactor: convert TxAlonzo.witness.signatures to Map, resolve some TODOs
mkazlauskas Nov 25, 2021
28f2ccf
refactor: rename MIR certificate 'address' to 'rewardAccount' and cha…
mkazlauskas Nov 25, 2021
4fc6ad8
refactor: improve Tip type to use the new BlockId type
mkazlauskas Nov 25, 2021
9781ae2
refactor: improve asset id types, convert TokenMap to Map
mkazlauskas Nov 26, 2021
ec523a7
fix: change stakepool metadata extVkey field type to bech32 string
mkazlauskas Nov 29, 2021
69e1f8e
chore: rm some obsolete TODO comments
mkazlauskas Nov 29, 2021
854e9cb
refactor: convert tx signature key type to hex
mkazlauskas Nov 29, 2021
d4200ef
refactor: change PoolParameters.vrf type to VrkVkHex
mkazlauskas Nov 29, 2021
914fab8
refactor(core): change type of requiredExtraSignatures to Ed25519KeyHash
mkazlauskas Nov 29, 2021
4ab2fda
refactor: improve name for PoolMdVk, improve opaque string tests
mkazlauskas Nov 29, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions packages/blockfrost/src/BlockfrostToCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const BlockfrostToCore = {

blockToTip: (block: Responses['block_content']): Cardano.Tip => ({
blockNo: block.height!,
hash: block.hash,
hash: Cardano.BlockId(block.hash),
slot: block.slot!
}),

Expand Down Expand Up @@ -58,7 +58,7 @@ export const BlockfrostToCore = {
transactionUtxos: (utxoResponse: Responses['tx_content_utxo']) => ({
inputs: utxoResponse.inputs.map((input) => ({
...BlockfrostToCore.txIn(input),
address: input.address
address: Cardano.Address(input.address)
})),
outputs: utxoResponse.outputs.map(BlockfrostToCore.txOut)
}),
Expand All @@ -70,19 +70,19 @@ export const BlockfrostToCore = {
}),

txIn: (blockfrost: BlockfrostInput): Cardano.TxIn => ({
address: blockfrost.address,
address: Cardano.Address(blockfrost.address),
index: blockfrost.output_index,
txId: blockfrost.tx_hash
txId: Cardano.TransactionId(blockfrost.tx_hash)
}),

txOut: (blockfrost: BlockfrostOutput): Cardano.TxOut => {
const assets: Cardano.Value['assets'] = {};
for (const amount of blockfrost.amount) {
if (amount.unit === 'lovelace') continue;
assets[amount.unit] = BigInt(amount.quantity);
const assets: Cardano.TokenMap = new Map();
for (const { quantity, unit } of blockfrost.amount) {
if (unit === 'lovelace') continue;
assets.set(Cardano.AssetId(unit), BigInt(quantity));
}
return {
address: blockfrost.address,
address: Cardano.Address(blockfrost.address),
value: {
assets,
coins: BigInt(blockfrost.amount.find(({ unit }) => unit === 'lovelace')!.quantity)
Expand Down
20 changes: 13 additions & 7 deletions packages/blockfrost/src/blockfrostAssetProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,32 +26,38 @@ const mapMetadata = (
export const blockfrostAssetProvider = (options: Options): AssetProvider => {
const blockfrost = new BlockFrostAPI(options);

const getAssetHistory = async (assetId: string): Promise<Cardano.AssetMintOrBurn[]> =>
const getAssetHistory = async (assetId: Cardano.AssetId): Promise<Cardano.AssetMintOrBurn[]> =>
fetchSequentially({
arg: assetId,
arg: assetId.toString(),
request: blockfrost.assetsHistory,
responseTranslator: (response): Cardano.AssetMintOrBurn[] =>
response.map(({ action, amount, tx_hash }) => ({
action: action === 'minted' ? Cardano.AssetProvisioning.Mint : Cardano.AssetProvisioning.Burn,
quantity: BigInt(amount),
transactionId: tx_hash
transactionId: Cardano.TransactionId(tx_hash)
}))
});

const getAsset: AssetProvider['getAsset'] = async (assetId) => {
const response = await blockfrost.assetsById(assetId);
const response = await blockfrost.assetsById(assetId.toString());
const name = Buffer.from(Asset.util.assetNameFromAssetId(assetId), 'hex').toString('utf-8');
const quantity = BigInt(response.quantity);
return {
assetId,
fingerprint: response.fingerprint,
fingerprint: Cardano.AssetFingerprint(response.fingerprint),
history:
response.mint_or_burn_count === 1
? [{ action: Cardano.AssetProvisioning.Mint, quantity, transactionId: response.initial_mint_tx_hash }]
? [
{
action: Cardano.AssetProvisioning.Mint,
quantity,
transactionId: Cardano.TransactionId(response.initial_mint_tx_hash)
}
]
: await getAssetHistory(assetId),
metadata: mapMetadata(response.onchain_metadata, response.metadata),
name,
policyId: response.policy_id,
policyId: Cardano.PolicyId(response.policy_id),
quantity
};
};
Expand Down
63 changes: 31 additions & 32 deletions packages/blockfrost/src/blockfrostWalletProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,18 +102,18 @@ export const blockfrostWalletProvider = (options: Options, logger = dummyLogger)
addresses.map(async (address) =>
fetchByAddressSequentially<Cardano.Utxo, BlockfrostUtxo>({
address,
request: (addr: Cardano.Address, pagination) => blockfrost.addressesUtxos(addr, pagination),
request: (addr: Cardano.Address, pagination) => blockfrost.addressesUtxos(addr.toString(), pagination),
responseTranslator: (addr: Cardano.Address, response: Responses['address_utxo_content']) =>
BlockfrostToCore.addressUtxoContent(addr, response)
BlockfrostToCore.addressUtxoContent(addr.toString(), response)
})
)
);
const utxo = utxoResults.flat(1);
if (rewardAccount !== undefined) {
try {
const accountResponse = await blockfrost.accounts(rewardAccount);
const accountResponse = await blockfrost.accounts(rewardAccount.toString());
const delegationAndRewards = {
delegate: accountResponse.pool_id || undefined,
delegate: accountResponse.pool_id ? Cardano.PoolId(accountResponse.pool_id) : undefined,
rewards: BigInt(accountResponse.withdrawable_amount)
};
return { delegationAndRewards, utxo };
Expand Down Expand Up @@ -150,8 +150,7 @@ export const blockfrostWalletProvider = (options: Options, logger = dummyLogger)
return purpose;
}
})(),
// TODO: need to confirm that this is correct encoding
scriptHash: Buffer.from(script_hash, 'hex').toString('base64')
scriptHash: Cardano.Hash28ByteBase16(script_hash)
})
);
};
Expand All @@ -165,7 +164,7 @@ export const blockfrostWalletProvider = (options: Options, logger = dummyLogger)
return response.map(
({ address, amount }): Cardano.Withdrawal => ({
quantity: BigInt(amount),
stakeAddress: address
stakeAddress: Cardano.RewardAccount(address)
})
);
};
Expand All @@ -183,7 +182,7 @@ export const blockfrostWalletProvider = (options: Options, logger = dummyLogger)
return response.map(({ pool_id, retiring_epoch }) => ({
__typename: Cardano.CertificateType.PoolRetirement,
epoch: retiring_epoch,
poolId: pool_id
poolId: Cardano.PoolId(pool_id)
}));
};

Expand All @@ -192,7 +191,7 @@ export const blockfrostWalletProvider = (options: Options, logger = dummyLogger)
return response.map(({ pool_id, active_epoch }) => ({
__typename: Cardano.CertificateType.PoolRegistration,
epoch: active_epoch,
poolId: pool_id,
poolId: Cardano.PoolId(pool_id),
poolParameters: ((): Cardano.PoolParameters => {
logger.warn('Omitting poolParameters for certificate in tx', hash);
return null as unknown as Cardano.PoolParameters;
Expand All @@ -204,10 +203,10 @@ export const blockfrostWalletProvider = (options: Options, logger = dummyLogger)
const response = await blockfrost.txsMirs(hash);
return response.map(({ address, amount, cert_index, pot }) => ({
__typename: Cardano.CertificateType.MIR,
address,
certIndex: cert_index,
pot,
quantity: BigInt(amount)
quantity: BigInt(amount),
rewardAccount: Cardano.RewardAccount(address)
}));
};

Expand All @@ -217,20 +216,20 @@ export const blockfrostWalletProvider = (options: Options, logger = dummyLogger)
__typename: registration
? Cardano.CertificateType.StakeKeyRegistration
: Cardano.CertificateType.StakeKeyDeregistration,
address,
certIndex: cert_index
certIndex: cert_index,
rewardAccount: Cardano.RewardAccount(address)
}));
};

const fetchDelegationCerts = async (hash: string): Promise<Cardano.StakeDelegationCertificate[]> => {
const response = await blockfrost.txsDelegations(hash);
return response.map(({ cert_index, index, address, active_epoch, pool_id }) => ({
__typename: Cardano.CertificateType.StakeDelegation,
address,
certIndex: cert_index,
delegationIndex: index,
epoch: active_epoch,
poolId: pool_id
poolId: Cardano.PoolId(pool_id),
rewardAccount: Cardano.RewardAccount(address)
}));
};

Expand All @@ -252,9 +251,9 @@ export const blockfrostWalletProvider = (options: Options, logger = dummyLogger)
];
};

const fetchJsonMetadata = async (txHash: Cardano.Hash16): Promise<Cardano.MetadatumMap | null> => {
const fetchJsonMetadata = async (txHash: Cardano.TransactionId): Promise<Cardano.MetadatumMap | null> => {
try {
const response = await blockfrost.txsMetadata(txHash);
const response = await blockfrost.txsMetadata(txHash.toString());
return response.reduce((map, metadatum) => {
// Not sure if types are correct, missing 'label', but it's present in docs
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand All @@ -273,9 +272,9 @@ export const blockfrostWalletProvider = (options: Options, logger = dummyLogger)

// eslint-disable-next-line unicorn/consistent-function-scoping
const parseValidityInterval = (num: string | null) => Number.parseInt(num || '') || undefined;
const fetchTransaction = async (hash: string): Promise<Cardano.TxAlonzo> => {
const { inputs, outputs } = BlockfrostToCore.transactionUtxos(await blockfrost.txsUtxos(hash));
const response = await blockfrost.txs(hash);
const fetchTransaction = async (hash: Cardano.TransactionId): Promise<Cardano.TxAlonzo> => {
const { inputs, outputs } = BlockfrostToCore.transactionUtxos(await blockfrost.txsUtxos(hash.toString()));
const response = await blockfrost.txs(hash.toString());
const metadata = await fetchJsonMetadata(hash);
return {
auxiliaryData: metadata
Expand All @@ -284,7 +283,7 @@ export const blockfrostWalletProvider = (options: Options, logger = dummyLogger)
}
: undefined,
blockHeader: {
blockHash: response.block,
blockHash: Cardano.BlockId(response.block),
blockHeight: response.block_height,
slot: response.slot
},
Expand All @@ -309,7 +308,7 @@ export const blockfrostWalletProvider = (options: Options, logger = dummyLogger)
txSize: response.size,
witness: {
redeemers: await fetchRedeemers(response),
signatures: {}
signatures: new Map() // not available in blockfrost
}
};
};
Expand All @@ -325,14 +324,14 @@ export const blockfrostWalletProvider = (options: Options, logger = dummyLogger)
BlockfrostTransactionContent
>({
address,
request: (addr: Cardano.Address, pagination) => blockfrost.addressesTransactions(addr, pagination)
request: (addr: Cardano.Address, pagination) => blockfrost.addressesTransactions(addr.toString(), pagination)
})
)
);

const transactionsArray = await Promise.all(
addressTransactions.map((transactionArray) =>
queryTransactionsByHashes(transactionArray.map(({ tx_hash }) => tx_hash))
queryTransactionsByHashes(transactionArray.map(({ tx_hash }) => Cardano.TransactionId(tx_hash)))
)
);

Expand All @@ -348,15 +347,15 @@ export const blockfrostWalletProvider = (options: Options, logger = dummyLogger)
};

const accountRewards = async (
stakeAddress: Cardano.Address,
stakeAddress: Cardano.RewardAccount,
{ lowerBound = 0, upperBound = Number.MAX_SAFE_INTEGER }: EpochRange = {}
): Promise<EpochRewards[]> => {
const result: EpochRewards[] = [];
const batchSize = 100;
let page = 1;
let haveMorePages = true;
while (haveMorePages) {
const rewards = await blockfrost.accountsRewards(stakeAddress, { count: batchSize, page });
const rewards = await blockfrost.accountsRewards(stakeAddress.toString(), { count: batchSize, page });
result.push(
...rewards
.filter(({ epoch }) => lowerBound <= epoch && epoch <= upperBound)
Expand Down Expand Up @@ -397,7 +396,7 @@ export const blockfrostWalletProvider = (options: Options, logger = dummyLogger)
};

const queryBlocksByHashes: WalletProvider['queryBlocksByHashes'] = async (hashes) => {
const responses = await Promise.all(hashes.map((hash) => blockfrost.blocks(hash)));
const responses = await Promise.all(hashes.map((hash) => blockfrost.blocks(hash.toString())));
return responses.map((response) => {
if (!response.epoch || !response.epoch_slot || !response.height || !response.slot || !response.block_vrf) {
throw new ProviderError(ProviderFailure.Unknown, null, 'Queried unsupported block');
Expand All @@ -409,17 +408,17 @@ export const blockfrostWalletProvider = (options: Options, logger = dummyLogger)
epochSlot: response.epoch_slot,
fees: BigInt(response.fees || '0'),
header: {
blockHash: response.hash,
blockHash: Cardano.BlockId(response.hash),
blockHeight: response.height,
slot: response.slot
},
nextBlock: response.next_block || undefined,
previousBlock: response.previous_block || undefined,
nextBlock: response.next_block ? Cardano.BlockId(response.next_block) : undefined,
previousBlock: response.previous_block ? Cardano.BlockId(response.previous_block) : undefined,
size: response.size,
slotLeader: response.slot_leader,
slotLeader: Cardano.PoolId(response.slot_leader),
totalOutput: BigInt(response.output || '0'),
txCount: response.tx_count,
vrf: response.block_vrf
vrf: Cardano.VrfVkBech32(response.block_vrf)
};
});
};
Expand Down
5 changes: 4 additions & 1 deletion packages/blockfrost/src/util.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Error as BlockfrostError } from '@blockfrost/blockfrost-js';
import { InvalidStringError, ProviderError, ProviderFailure } from '@cardano-sdk/core';
import { PaginationOptions } from '@blockfrost/blockfrost-js/lib/types';
import { ProviderError, ProviderFailure } from '@cardano-sdk/core';

export const formatBlockfrostError = (error: unknown) => {
const blockfrostError = error as BlockfrostError;
Expand All @@ -11,6 +11,9 @@ export const formatBlockfrostError = (error: unknown) => {
if (typeof blockfrostError !== 'object') {
throw new ProviderError(ProviderFailure.Unknown, error, 'failed to parse error (response type)');
}
if (error instanceof InvalidStringError) {
throw new ProviderError(ProviderFailure.InvalidResponse, error);
}
const errorAsType1 = blockfrostError as {
status_code: number;
message: string;
Expand Down
30 changes: 17 additions & 13 deletions packages/blockfrost/test/blockfrostAssetProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,18 @@ describe('blockfrostAssetProvider', () => {
BlockFrostAPI.prototype.assetsById = jest.fn().mockResolvedValue(mockedAssetResponse);

const client = blockfrostAssetProvider({ isTestnet: true, projectId: apiKey });
const response = await client.getAsset('b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e');
const response = await client.getAsset(
Cardano.AssetId('b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e')
);

expect(response).toMatchObject<Cardano.Asset>({
assetId: 'b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e',
fingerprint: 'asset1pkpwyknlvul7az0xx8czhl60pyel45rpje4z8w',
assetId: Cardano.AssetId('b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e'),
fingerprint: Cardano.AssetFingerprint('asset1pkpwyknlvul7az0xx8czhl60pyel45rpje4z8w'),
history: [
{
action: Cardano.AssetProvisioning.Mint,
quantity: 12_000n,
transactionId: '6804edf9712d2b619edb6ac86861fe93a730693183a262b165fcc1ba1bc99cad'
transactionId: Cardano.TransactionId('6804edf9712d2b619edb6ac86861fe93a730693183a262b165fcc1ba1bc99cad')
}
],
metadata: {
Expand All @@ -58,7 +60,7 @@ describe('blockfrostAssetProvider', () => {
url: 'https://www.stakenuts.com/'
},
name: 'nutcoin',
policyId: 'b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a7',
policyId: Cardano.PolicyId('b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a7'),
quantity: 12_000n
});
});
Expand All @@ -72,31 +74,33 @@ describe('blockfrostAssetProvider', () => {
{
action: 'minted',
amount: '13000',
tx_hash: 'tx1hash'
tx_hash: '4123d70f66414cc921f6ffc29a899aafc7137a99a0fd453d6b200863ef5702d6'
},
{
action: 'burned',
amount: '1000',
tx_hash: 'tx2hash'
tx_hash: '01d7366549986d83edeea262e97b68eca3430d3bb052ed1c37d2202fd5458872'
}
] as Responses['asset_history']);

const client = blockfrostAssetProvider({ isTestnet: true, projectId: apiKey });
const response = await client.getAsset('b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e');
const response = await client.getAsset(
Cardano.AssetId('b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e')
);

expect(response).toMatchObject<Cardano.Asset>({
assetId: 'b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e',
fingerprint: 'asset1pkpwyknlvul7az0xx8czhl60pyel45rpje4z8w',
assetId: Cardano.AssetId('b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e'),
fingerprint: Cardano.AssetFingerprint('asset1pkpwyknlvul7az0xx8czhl60pyel45rpje4z8w'),
history: [
{
action: Cardano.AssetProvisioning.Mint,
quantity: 13_000n,
transactionId: 'tx1hash'
transactionId: Cardano.TransactionId('4123d70f66414cc921f6ffc29a899aafc7137a99a0fd453d6b200863ef5702d6')
},
{
action: Cardano.AssetProvisioning.Burn,
quantity: 1000n,
transactionId: 'tx2hash'
transactionId: Cardano.TransactionId('01d7366549986d83edeea262e97b68eca3430d3bb052ed1c37d2202fd5458872')
}
],
metadata: {
Expand All @@ -108,7 +112,7 @@ describe('blockfrostAssetProvider', () => {
url: 'https://www.stakenuts.com/'
},
name: 'nutcoin',
policyId: 'b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a7',
policyId: Cardano.PolicyId('b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a7'),
quantity: 12_000n
});
});
Expand Down
Loading