|
| 1 | +import * as supertest from 'supertest'; |
| 2 | +import { ChainID } from '@stacks/transactions'; |
| 3 | +import { |
| 4 | + DbBlock, |
| 5 | + DbTx, |
| 6 | + DbTxTypeId, |
| 7 | + DbStxEvent, |
| 8 | + DbEventTypeId, |
| 9 | + DbAssetEventTypeId, |
| 10 | + DbMinerReward, |
| 11 | +} from '../datastore/common'; |
| 12 | +import { startApiServer, ApiServer } from '../api/init'; |
| 13 | +import { bufferToHexPrefixString, I32_MAX, microStxToStx, STACKS_DECIMAL_PLACES } from '../helpers'; |
| 14 | +import { FEE_RATE } from '../api/routes/fee-rate'; |
| 15 | +import { FeeRateRequest } from 'docs/generated'; |
| 16 | +import { PgWriteStore } from '../datastore/pg-write-store'; |
| 17 | +import { cycleMigrations, runMigrations } from '../datastore/migrations'; |
| 18 | +import { PgSqlClient } from '../datastore/connection'; |
| 19 | + |
| 20 | +describe('other tests', () => { |
| 21 | + let db: PgWriteStore; |
| 22 | + let client: PgSqlClient; |
| 23 | + let api: ApiServer; |
| 24 | + |
| 25 | + beforeEach(async () => { |
| 26 | + process.env.PG_DATABASE = 'postgres'; |
| 27 | + await cycleMigrations(); |
| 28 | + db = await PgWriteStore.connect({ |
| 29 | + usageName: 'tests', |
| 30 | + withNotifier: false, |
| 31 | + skipMigrations: true, |
| 32 | + }); |
| 33 | + client = db.sql; |
| 34 | + api = await startApiServer({ datastore: db, chainId: ChainID.Testnet, httpLogLevel: 'silly' }); |
| 35 | + }); |
| 36 | + |
| 37 | + test('stx-supply', async () => { |
| 38 | + const testAddr1 = 'testAddr1'; |
| 39 | + const dbBlock1: DbBlock = { |
| 40 | + block_hash: '0x0123', |
| 41 | + index_block_hash: '0x1234', |
| 42 | + parent_index_block_hash: '0x00', |
| 43 | + parent_block_hash: '0x5678', |
| 44 | + parent_microblock_hash: '', |
| 45 | + parent_microblock_sequence: 0, |
| 46 | + block_height: 1, |
| 47 | + burn_block_time: 39486, |
| 48 | + burn_block_hash: '0x1234', |
| 49 | + burn_block_height: 123, |
| 50 | + miner_txid: '0x4321', |
| 51 | + canonical: true, |
| 52 | + execution_cost_read_count: 0, |
| 53 | + execution_cost_read_length: 0, |
| 54 | + execution_cost_runtime: 0, |
| 55 | + execution_cost_write_count: 0, |
| 56 | + execution_cost_write_length: 0, |
| 57 | + }; |
| 58 | + const tx: DbTx = { |
| 59 | + tx_id: '0x1234', |
| 60 | + tx_index: 4, |
| 61 | + anchor_mode: 3, |
| 62 | + nonce: 0, |
| 63 | + raw_tx: '', |
| 64 | + index_block_hash: dbBlock1.index_block_hash, |
| 65 | + block_hash: dbBlock1.block_hash, |
| 66 | + block_height: dbBlock1.block_height, |
| 67 | + burn_block_time: dbBlock1.burn_block_time, |
| 68 | + parent_burn_block_time: 0, |
| 69 | + type_id: DbTxTypeId.Coinbase, |
| 70 | + coinbase_payload: bufferToHexPrefixString(Buffer.from('coinbase hi')), |
| 71 | + status: 1, |
| 72 | + raw_result: '0x0100000000000000000000000000000001', // u1 |
| 73 | + canonical: true, |
| 74 | + microblock_canonical: true, |
| 75 | + microblock_sequence: I32_MAX, |
| 76 | + microblock_hash: '', |
| 77 | + parent_index_block_hash: '', |
| 78 | + parent_block_hash: '', |
| 79 | + post_conditions: '0x01f5', |
| 80 | + fee_rate: 1234n, |
| 81 | + sponsored: false, |
| 82 | + sponsor_address: undefined, |
| 83 | + sender_address: testAddr1, |
| 84 | + origin_hash_mode: 1, |
| 85 | + event_count: 5, |
| 86 | + execution_cost_read_count: 0, |
| 87 | + execution_cost_read_length: 0, |
| 88 | + execution_cost_runtime: 0, |
| 89 | + execution_cost_write_count: 0, |
| 90 | + execution_cost_write_length: 0, |
| 91 | + }; |
| 92 | + const stxMintEvent1: DbStxEvent = { |
| 93 | + event_index: 0, |
| 94 | + tx_id: tx.tx_id, |
| 95 | + tx_index: tx.tx_index, |
| 96 | + block_height: tx.block_height, |
| 97 | + canonical: true, |
| 98 | + asset_event_type_id: DbAssetEventTypeId.Mint, |
| 99 | + recipient: tx.sender_address, |
| 100 | + event_type: DbEventTypeId.StxAsset, |
| 101 | + amount: 230_000_000_000_000n, |
| 102 | + }; |
| 103 | + const stxMintEvent2: DbStxEvent = { |
| 104 | + ...stxMintEvent1, |
| 105 | + amount: 5_000_000_000_000n, |
| 106 | + }; |
| 107 | + await db.update({ |
| 108 | + block: dbBlock1, |
| 109 | + microblocks: [], |
| 110 | + minerRewards: [], |
| 111 | + txs: [ |
| 112 | + { |
| 113 | + tx: tx, |
| 114 | + stxEvents: [stxMintEvent1, stxMintEvent2], |
| 115 | + stxLockEvents: [], |
| 116 | + ftEvents: [], |
| 117 | + nftEvents: [], |
| 118 | + contractLogEvents: [], |
| 119 | + names: [], |
| 120 | + namespaces: [], |
| 121 | + smartContracts: [], |
| 122 | + }, |
| 123 | + ], |
| 124 | + }); |
| 125 | + |
| 126 | + const expectedTotalStx1 = stxMintEvent1.amount + stxMintEvent2.amount; |
| 127 | + const result1 = await supertest(api.server).get(`/extended/v1/stx_supply`); |
| 128 | + expect(result1.status).toBe(200); |
| 129 | + expect(result1.type).toBe('application/json'); |
| 130 | + const expectedResp1 = { |
| 131 | + unlocked_percent: '17.38', |
| 132 | + total_stx: '1352464600.000000', |
| 133 | + unlocked_stx: microStxToStx(expectedTotalStx1), |
| 134 | + block_height: dbBlock1.block_height, |
| 135 | + }; |
| 136 | + expect(JSON.parse(result1.text)).toEqual(expectedResp1); |
| 137 | + |
| 138 | + // ensure burned STX reduce the unlocked stx supply |
| 139 | + const stxBurnEvent1: DbStxEvent = { |
| 140 | + event_index: 0, |
| 141 | + tx_id: tx.tx_id, |
| 142 | + tx_index: tx.tx_index, |
| 143 | + block_height: tx.block_height, |
| 144 | + canonical: true, |
| 145 | + asset_event_type_id: DbAssetEventTypeId.Burn, |
| 146 | + sender: tx.sender_address, |
| 147 | + event_type: DbEventTypeId.StxAsset, |
| 148 | + amount: 10_000_000_000_000n, |
| 149 | + }; |
| 150 | + await db.updateStxEvent(client, tx, stxBurnEvent1); |
| 151 | + const expectedTotalStx2 = stxMintEvent1.amount + stxMintEvent2.amount - stxBurnEvent1.amount; |
| 152 | + const result2 = await supertest(api.server).get(`/extended/v1/stx_supply`); |
| 153 | + expect(result2.status).toBe(200); |
| 154 | + expect(result2.type).toBe('application/json'); |
| 155 | + const expectedResp2 = { |
| 156 | + unlocked_percent: '16.64', |
| 157 | + total_stx: '1352464600.000000', |
| 158 | + unlocked_stx: microStxToStx(expectedTotalStx2), |
| 159 | + block_height: dbBlock1.block_height, |
| 160 | + }; |
| 161 | + expect(JSON.parse(result2.text)).toEqual(expectedResp2); |
| 162 | + |
| 163 | + // ensure miner coinbase rewards are included |
| 164 | + const minerReward1: DbMinerReward = { |
| 165 | + block_hash: dbBlock1.block_hash, |
| 166 | + index_block_hash: dbBlock1.index_block_hash, |
| 167 | + from_index_block_hash: dbBlock1.index_block_hash, |
| 168 | + mature_block_height: dbBlock1.block_height, |
| 169 | + canonical: true, |
| 170 | + recipient: testAddr1, |
| 171 | + coinbase_amount: 15_000_000_000_000n, |
| 172 | + tx_fees_anchored: 1_000_000_000_000n, |
| 173 | + tx_fees_streamed_confirmed: 2_000_000_000_000n, |
| 174 | + tx_fees_streamed_produced: 3_000_000_000_000n, |
| 175 | + }; |
| 176 | + await db.updateMinerReward(client, minerReward1); |
| 177 | + const expectedTotalStx3 = |
| 178 | + stxMintEvent1.amount + |
| 179 | + stxMintEvent2.amount - |
| 180 | + stxBurnEvent1.amount + |
| 181 | + minerReward1.coinbase_amount; |
| 182 | + const result3 = await supertest(api.server).get(`/extended/v1/stx_supply`); |
| 183 | + expect(result3.status).toBe(200); |
| 184 | + expect(result3.type).toBe('application/json'); |
| 185 | + const expectedResp3 = { |
| 186 | + unlocked_percent: '17.75', |
| 187 | + total_stx: '1352464600.000000', |
| 188 | + unlocked_stx: microStxToStx(expectedTotalStx3), |
| 189 | + block_height: dbBlock1.block_height, |
| 190 | + }; |
| 191 | + expect(JSON.parse(result3.text)).toEqual(expectedResp3); |
| 192 | + |
| 193 | + const result4 = await supertest(api.server).get(`/extended/v1/stx_supply/total/plain`); |
| 194 | + expect(result4.status).toBe(200); |
| 195 | + expect(result4.type).toBe('text/plain'); |
| 196 | + expect(result4.text).toEqual('1352464600.000000'); |
| 197 | + |
| 198 | + const result5 = await supertest(api.server).get(`/extended/v1/stx_supply/circulating/plain`); |
| 199 | + expect(result5.status).toBe(200); |
| 200 | + expect(result5.type).toBe('text/plain'); |
| 201 | + expect(result5.text).toEqual(microStxToStx(expectedTotalStx3)); |
| 202 | + |
| 203 | + // test legacy endpoint response formatting |
| 204 | + const result6 = await supertest(api.server).get(`/extended/v1/stx_supply/legacy_format`); |
| 205 | + expect(result6.status).toBe(200); |
| 206 | + expect(result6.type).toBe('application/json'); |
| 207 | + const expectedResp6 = { |
| 208 | + unlockedPercent: '17.75', |
| 209 | + totalStacks: '1352464600.000000', |
| 210 | + totalStacksFormatted: '1,352,464,600.000000', |
| 211 | + unlockedSupply: microStxToStx(expectedTotalStx3), |
| 212 | + unlockedSupplyFormatted: new Intl.NumberFormat('en', { |
| 213 | + minimumFractionDigits: STACKS_DECIMAL_PLACES, |
| 214 | + }).format(parseInt(microStxToStx(expectedTotalStx3))), |
| 215 | + blockHeight: dbBlock1.block_height.toString(), |
| 216 | + }; |
| 217 | + expect(JSON.parse(result6.text)).toEqual(expectedResp6); |
| 218 | + }); |
| 219 | + |
| 220 | + test('Get fee rate', async () => { |
| 221 | + const request: FeeRateRequest = { |
| 222 | + transaction: '0x5e9f3933e358df6a73fec0d47ce3e1062c20812c129f5294e6f37a8d27c051d9', |
| 223 | + }; |
| 224 | + const result = await supertest(api.server).post('/extended/v1/fee_rate').send(request); |
| 225 | + expect(result.status).toBe(200); |
| 226 | + expect(result.type).toBe('application/json'); |
| 227 | + expect(result.body.fee_rate).toBe(FEE_RATE); |
| 228 | + }); |
| 229 | + |
| 230 | + test('400 response errors', async () => { |
| 231 | + const tx_id = '0x8407751d1a8d11ee986aca32a6459d9cd798283a12e048ebafcd4cc7dadb29a'; |
| 232 | + const block_hash = '0xd10ccecfd7ac9e5f8a10de0532fac028559b31a6ff494d82147f6297fb66313'; |
| 233 | + const principal_addr = 'S.hello-world'; |
| 234 | + const odd_tx_error = { |
| 235 | + error: `Hex string is an odd number of digits: ${tx_id}`, |
| 236 | + }; |
| 237 | + const odd_block_error = { |
| 238 | + error: `Hex string is an odd number of digits: ${block_hash}`, |
| 239 | + }; |
| 240 | + const metadata_error = { error: `Unexpected value for 'include_metadata' parameter: "bac"` }; |
| 241 | + const principal_error = { error: 'invalid STX address "S.hello-world"' }; |
| 242 | + const pagination_error = { error: '`limit` must be equal to or less than 200' }; |
| 243 | + // extended/v1/tx |
| 244 | + const searchResult1 = await supertest(api.server).get(`/extended/v1/tx/${tx_id}`); |
| 245 | + expect(JSON.parse(searchResult1.text)).toEqual(odd_tx_error); |
| 246 | + expect(searchResult1.status).toBe(400); |
| 247 | + const searchResult2 = await supertest(api.server).get( |
| 248 | + `/extended/v1/tx/multiple?tx_id=${tx_id}` |
| 249 | + ); |
| 250 | + expect(JSON.parse(searchResult2.text)).toEqual(odd_tx_error); |
| 251 | + expect(searchResult2.status).toBe(400); |
| 252 | + const searchResult3 = await supertest(api.server).get(`/extended/v1/tx/${tx_id}/raw`); |
| 253 | + expect(JSON.parse(searchResult3.text)).toEqual(odd_tx_error); |
| 254 | + expect(searchResult3.status).toBe(400); |
| 255 | + const searchResult4 = await supertest(api.server).get(`/extended/v1/tx/block/${block_hash}`); |
| 256 | + expect(JSON.parse(searchResult4.text)).toEqual(odd_block_error); |
| 257 | + expect(searchResult4.status).toBe(400); |
| 258 | + |
| 259 | + // extended/v1/block |
| 260 | + const searchResult5 = await supertest(api.server).get(`/extended/v1/block/${block_hash}`); |
| 261 | + expect(JSON.parse(searchResult5.text)).toEqual(odd_block_error); |
| 262 | + expect(searchResult5.status).toBe(400); |
| 263 | + |
| 264 | + // extended/v1/microblock |
| 265 | + const searchResult6 = await supertest(api.server).get(`/extended/v1/microblock/${block_hash}`); |
| 266 | + expect(JSON.parse(searchResult6.text)).toEqual(odd_block_error); |
| 267 | + expect(searchResult6.status).toBe(400); |
| 268 | + |
| 269 | + // extended/v1/search |
| 270 | + const searchResult7 = await supertest(api.server).get( |
| 271 | + `/extended/v1/search/${block_hash}?include_metadata=bac` |
| 272 | + ); |
| 273 | + expect(JSON.parse(searchResult7.text)).toEqual(metadata_error); |
| 274 | + expect(searchResult7.status).toBe(400); |
| 275 | + |
| 276 | + // extended/v1/address |
| 277 | + const searchResult8 = await supertest(api.server).get( |
| 278 | + `/extended/v1/address/${principal_addr}/stx` |
| 279 | + ); |
| 280 | + expect(JSON.parse(searchResult8.text)).toEqual(principal_error); |
| 281 | + expect(searchResult8.status).toBe(400); |
| 282 | + |
| 283 | + // pagination queries |
| 284 | + const searchResult9 = await supertest(api.server).get( |
| 285 | + '/extended/v1/tx/mempool?limit=201&offset=2' |
| 286 | + ); |
| 287 | + expect(JSON.parse(searchResult9.text)).toEqual(pagination_error); |
| 288 | + expect(searchResult9.status).toBe(400); |
| 289 | + }); |
| 290 | + |
| 291 | + test('active status', async () => { |
| 292 | + const result = await supertest(api.server).get(`/extended/v1/status/`); |
| 293 | + expect(result.body.status).toBe('ready'); |
| 294 | + }); |
| 295 | + |
| 296 | + afterEach(async () => { |
| 297 | + await api.terminate(); |
| 298 | + await db?.close(); |
| 299 | + await runMigrations(undefined, 'down'); |
| 300 | + }); |
| 301 | +}); |
0 commit comments