mirror of
https://github.com/alexgo-io/stacks-blockchain-api.git
synced 2026-01-12 22:43:34 +08:00
This reverts commit 65bb4e55fa.
This commit is contained in:
13
docs/api/address/get-address-nft-events.example.json
Normal file
13
docs/api/address/get-address-nft-events.example.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"limit": 20,
|
||||
"offset": 0,
|
||||
"total": 1,
|
||||
"nft_events": [
|
||||
{
|
||||
"sender": "none",
|
||||
"recipient": "ST1HB64MAJ1MBV4CQ80GF01DZS4T1DSMX20ADCRA4",
|
||||
"asset_identifier": "some-asset",
|
||||
"value": { "hex": "0x00", "repr": "0" }
|
||||
}
|
||||
]
|
||||
}
|
||||
28
docs/api/address/get-address-nft-events.schema.json
Normal file
28
docs/api/address/get-address-nft-events.schema.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"type": "object",
|
||||
"title": "AddressNftListResponse",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"limit",
|
||||
"offset",
|
||||
"total",
|
||||
"nft_events"
|
||||
],
|
||||
"properties": {
|
||||
"limit": {
|
||||
"type": "integer"
|
||||
},
|
||||
"offset": {
|
||||
"type": "integer"
|
||||
},
|
||||
"total": {
|
||||
"type": "integer"
|
||||
},
|
||||
"nft_events": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "../../entities/nft-events/nft-event.schema.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1643,6 +1643,63 @@ paths:
|
||||
example:
|
||||
$ref: ./api/address/get-address-stx-inbound.example.json
|
||||
|
||||
/extended/v1/address/{principal}/nft_events:
|
||||
get:
|
||||
summary: Get nft events
|
||||
deprecated: true
|
||||
description: |
|
||||
**NOTE:** This endpoint is deprecated in favor of [Non-Fungible Token holdings](#operation/get_nft_holdings).
|
||||
|
||||
Retrieves a list of all nfts owned by an address, contains the clarity value of the identifier of the nft.
|
||||
tags:
|
||||
- Accounts
|
||||
operationId: get_account_nft
|
||||
parameters:
|
||||
- name: principal
|
||||
in: path
|
||||
description: Stacks address or a Contract identifier
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: "SP31DA6FTSJX2WGTZ69SFY11BH51NZMB0ZW97B5P0"
|
||||
- name: limit
|
||||
in: query
|
||||
description: number of items to return
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
- name: offset
|
||||
in: query
|
||||
description: number of items to skip
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
example: 42000
|
||||
- name: unanchored
|
||||
in: query
|
||||
description: Include transaction data from unanchored (i.e. unconfirmed) microblocks
|
||||
required: false
|
||||
schema:
|
||||
type: boolean
|
||||
example: true
|
||||
default: false
|
||||
- name: until_block
|
||||
in: query
|
||||
description: returned data representing the state up until that point in time, rather than the current block. Note - Use either of the query parameters but not both at a time.
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
example: 60000
|
||||
responses:
|
||||
200:
|
||||
description: Success
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ./api/address/get-address-nft-events.schema.json
|
||||
example:
|
||||
$ref: ./api/address/get-address-nft-events.example.json
|
||||
|
||||
/v2/accounts/{principal}:
|
||||
get:
|
||||
summary: Get account info
|
||||
|
||||
@@ -476,6 +476,59 @@ export function createAddressRouter(db: PgStore, chainId: ChainID): express.Rout
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* @deprecated Use `/extended/v1/tokens/nft/holdings` instead.
|
||||
*/
|
||||
router.get(
|
||||
'/:stx_address/nft_events',
|
||||
cacheHandler,
|
||||
asyncHandler(async (req, res, next) => {
|
||||
// get recent asset event associated with address
|
||||
const stxAddress = req.params['stx_address'];
|
||||
validatePrincipal(stxAddress);
|
||||
|
||||
const untilBlock = parseUntilBlockQuery(req, res, next);
|
||||
const blockHeight = await getBlockHeight(untilBlock, req, res, next, db);
|
||||
const limit = parseAssetsQueryLimit(req.query.limit ?? 20);
|
||||
const offset = parsePagingQueryInput(req.query.offset ?? 0);
|
||||
const includeUnanchored = isUnanchoredRequest(req, res, next);
|
||||
|
||||
const response = await db.getAddressNFTEvent({
|
||||
stxAddress,
|
||||
limit,
|
||||
offset,
|
||||
blockHeight,
|
||||
includeUnanchored,
|
||||
});
|
||||
const nft_events = response.results.map(row => {
|
||||
const parsedClarityValue = decodeClarityValueToRepr(row.value);
|
||||
const r: NftEvent = {
|
||||
sender: row.sender,
|
||||
recipient: row.recipient,
|
||||
asset_identifier: row.asset_identifier,
|
||||
value: {
|
||||
hex: row.value,
|
||||
repr: parsedClarityValue,
|
||||
},
|
||||
tx_id: row.tx_id,
|
||||
block_height: row.block_height,
|
||||
event_index: row.event_index,
|
||||
asset_event_type: getAssetEventTypeString(row.asset_event_type_id),
|
||||
tx_index: row.tx_index,
|
||||
};
|
||||
return r;
|
||||
});
|
||||
const nftListResponse: AddressNftListResponse = {
|
||||
nft_events: nft_events,
|
||||
total: response.total,
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
};
|
||||
setETagCacheHeaders(res);
|
||||
res.json(nftListResponse);
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:address/mempool',
|
||||
mempoolCacheHandler,
|
||||
|
||||
@@ -3066,6 +3066,56 @@ export class PgStore {
|
||||
return { found: true, result: result[0] } as const;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `getNftHoldings` instead.
|
||||
*/
|
||||
async getAddressNFTEvent(args: {
|
||||
stxAddress: string;
|
||||
limit: number;
|
||||
offset: number;
|
||||
blockHeight: number;
|
||||
includeUnanchored: boolean;
|
||||
}): Promise<{ results: AddressNftEventIdentifier[]; total: number }> {
|
||||
// Join against `nft_custody` materialized view only if we're looking for canonical results.
|
||||
const result = await this.sql<(AddressNftEventIdentifier & { count: number })[]>`
|
||||
WITH address_transfers AS (
|
||||
SELECT asset_identifier, value, sender, recipient, block_height, microblock_sequence, tx_index, event_index, tx_id, asset_event_type_id
|
||||
FROM nft_events
|
||||
WHERE canonical = true AND microblock_canonical = true
|
||||
AND recipient = ${args.stxAddress} AND block_height <= ${args.blockHeight}
|
||||
),
|
||||
last_nft_transfers AS (
|
||||
SELECT DISTINCT ON(asset_identifier, value) asset_identifier, value, recipient
|
||||
FROM nft_events
|
||||
WHERE canonical = true AND microblock_canonical = true
|
||||
AND block_height <= ${args.blockHeight}
|
||||
ORDER BY asset_identifier, value, block_height DESC, microblock_sequence DESC, tx_index DESC, event_index DESC
|
||||
)
|
||||
SELECT sender, recipient, asset_identifier, value, event_index, asset_event_type_id, address_transfers.block_height, address_transfers.tx_id, (COUNT(*) OVER())::INTEGER AS count
|
||||
FROM address_transfers
|
||||
INNER JOIN ${args.includeUnanchored ? this.sql`last_nft_transfers` : this.sql`nft_custody`}
|
||||
USING (asset_identifier, value, recipient)
|
||||
ORDER BY block_height DESC, microblock_sequence DESC, tx_index DESC, event_index DESC
|
||||
LIMIT ${args.limit} OFFSET ${args.offset}
|
||||
`;
|
||||
|
||||
const count = result.length > 0 ? result[0].count : 0;
|
||||
|
||||
const nftEvents = result.map(row => ({
|
||||
sender: row.sender,
|
||||
recipient: row.recipient,
|
||||
asset_identifier: row.asset_identifier,
|
||||
value: row.value,
|
||||
block_height: row.block_height,
|
||||
tx_id: row.tx_id,
|
||||
event_index: row.event_index,
|
||||
asset_event_type_id: row.asset_event_type_id,
|
||||
tx_index: row.tx_index,
|
||||
}));
|
||||
|
||||
return { results: nftEvents, total: count };
|
||||
}
|
||||
|
||||
async getTxListDetails({
|
||||
txIds,
|
||||
includeUnanchored,
|
||||
|
||||
@@ -2235,6 +2235,7 @@ describe('address tests', () => {
|
||||
'/transactions_with_transfers',
|
||||
'/assets',
|
||||
'/stx_inbound',
|
||||
'/nft_events',
|
||||
];
|
||||
|
||||
//check for mutually exclusive unachored and and until_block
|
||||
@@ -2256,6 +2257,233 @@ describe('address tests', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('Success: nft events for address', async () => {
|
||||
const addr1 = 'ST3J8EVYHVKH6XXPD61EE8XEHW4Y2K83861225AB1';
|
||||
const addr2 = 'ST1HB64MAJ1MBV4CQ80GF01DZS4T1DSMX20ADCRA4';
|
||||
|
||||
const dbBlock: DbBlock = {
|
||||
block_hash: '0xff',
|
||||
index_block_hash: '0x1234',
|
||||
parent_index_block_hash: '0x5678',
|
||||
parent_block_hash: '0x5678',
|
||||
parent_microblock_hash: '',
|
||||
parent_microblock_sequence: 0,
|
||||
block_height: 1,
|
||||
burn_block_time: 1594647995,
|
||||
burn_block_hash: '0x1234',
|
||||
burn_block_height: 123,
|
||||
miner_txid: '0x4321',
|
||||
canonical: true,
|
||||
execution_cost_read_count: 0,
|
||||
execution_cost_read_length: 0,
|
||||
execution_cost_runtime: 0,
|
||||
execution_cost_write_count: 0,
|
||||
execution_cost_write_length: 0,
|
||||
};
|
||||
const stxTx: DbTx = {
|
||||
tx_id: '0x1111000000000000000000000000000000000000000000000000000000000000',
|
||||
tx_index: 0,
|
||||
anchor_mode: 3,
|
||||
nonce: 0,
|
||||
raw_tx: '0x',
|
||||
index_block_hash: dbBlock.index_block_hash,
|
||||
block_hash: dbBlock.block_hash,
|
||||
block_height: dbBlock.block_height,
|
||||
burn_block_time: dbBlock.burn_block_time,
|
||||
parent_burn_block_time: 1626122935,
|
||||
type_id: DbTxTypeId.TokenTransfer,
|
||||
token_transfer_amount: 1n,
|
||||
token_transfer_memo: bufferToHexPrefixString(Buffer.from('hi')),
|
||||
token_transfer_recipient_address: 'none',
|
||||
status: 1,
|
||||
raw_result: '0x0100000000000000000000000000000001', // u1
|
||||
canonical: true,
|
||||
microblock_canonical: true,
|
||||
microblock_sequence: I32_MAX,
|
||||
microblock_hash: '',
|
||||
parent_index_block_hash: dbBlock.parent_index_block_hash,
|
||||
parent_block_hash: dbBlock.parent_block_hash,
|
||||
post_conditions: '0x01f5',
|
||||
fee_rate: 1234n,
|
||||
sponsored: false,
|
||||
sponsor_address: undefined,
|
||||
sender_address: addr1,
|
||||
origin_hash_mode: 1,
|
||||
event_count: 10,
|
||||
execution_cost_read_count: 0,
|
||||
execution_cost_read_length: 0,
|
||||
execution_cost_runtime: 0,
|
||||
execution_cost_write_count: 0,
|
||||
execution_cost_write_length: 0,
|
||||
};
|
||||
const nftEvents: DbNftEvent[] = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
nftEvents.push({
|
||||
canonical: true,
|
||||
event_type: DbEventTypeId.NonFungibleTokenAsset,
|
||||
asset_event_type_id: DbAssetEventTypeId.Transfer,
|
||||
event_index: 0,
|
||||
tx_id: stxTx.tx_id,
|
||||
tx_index: 1,
|
||||
block_height: dbBlock.block_height,
|
||||
asset_identifier: 'some-asset',
|
||||
value: '0x0000000000000000000000000000000000',
|
||||
recipient: addr1,
|
||||
sender: 'none',
|
||||
});
|
||||
}
|
||||
|
||||
await db.update({
|
||||
block: dbBlock,
|
||||
microblocks: [],
|
||||
minerRewards: [],
|
||||
txs: [
|
||||
{
|
||||
tx: stxTx,
|
||||
stxLockEvents: [],
|
||||
stxEvents: [],
|
||||
ftEvents: [],
|
||||
nftEvents: nftEvents,
|
||||
contractLogEvents: [],
|
||||
smartContracts: [],
|
||||
names: [],
|
||||
namespaces: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const limit = 2;
|
||||
const offset = 0;
|
||||
// test nft for given addresses
|
||||
const result = await supertest(api.server).get(
|
||||
`/extended/v1/address/${addr1}/nft_events?limit=${limit}&offset=${offset}`
|
||||
);
|
||||
expect(result.status).toBe(200);
|
||||
expect(result.type).toBe('application/json');
|
||||
expect(result.body.total).toEqual(10);
|
||||
expect(result.body.nft_events.length).toEqual(2);
|
||||
expect(result.body.nft_events[0].recipient).toBe(addr1);
|
||||
expect(result.body.nft_events[0].tx_id).toBe(
|
||||
'0x1111000000000000000000000000000000000000000000000000000000000000'
|
||||
);
|
||||
expect(result.body.nft_events[0].block_height).toBe(1);
|
||||
expect(result.body.nft_events[0].value.repr).toBe('0');
|
||||
|
||||
const dbBlock2: DbBlock = {
|
||||
block_hash: '0xffff',
|
||||
index_block_hash: '0x123466',
|
||||
parent_index_block_hash: '0x1234',
|
||||
parent_block_hash: '0xff',
|
||||
parent_microblock_hash: '',
|
||||
parent_microblock_sequence: 0,
|
||||
block_height: 2,
|
||||
burn_block_time: 1594649995,
|
||||
burn_block_hash: '0x123456',
|
||||
burn_block_height: 124,
|
||||
miner_txid: '0x4321',
|
||||
canonical: true,
|
||||
execution_cost_read_count: 0,
|
||||
execution_cost_read_length: 0,
|
||||
execution_cost_runtime: 0,
|
||||
execution_cost_write_count: 0,
|
||||
execution_cost_write_length: 0,
|
||||
};
|
||||
const stxTx1: DbTx = {
|
||||
tx_id: '0x1111100000000000000000000000000000000000000000000000000000000001',
|
||||
tx_index: 0,
|
||||
anchor_mode: 3,
|
||||
nonce: 0,
|
||||
raw_tx: '0x',
|
||||
index_block_hash: dbBlock2.index_block_hash,
|
||||
block_hash: dbBlock2.block_hash,
|
||||
block_height: dbBlock2.block_height,
|
||||
burn_block_time: dbBlock2.burn_block_time,
|
||||
parent_burn_block_time: 1626124935,
|
||||
type_id: DbTxTypeId.TokenTransfer,
|
||||
token_transfer_amount: 1n,
|
||||
token_transfer_memo: bufferToHexPrefixString(Buffer.from('hi')),
|
||||
token_transfer_recipient_address: 'none',
|
||||
status: 1,
|
||||
raw_result: '0x0100000000000000000000000000000001', // u1
|
||||
canonical: true,
|
||||
microblock_canonical: true,
|
||||
microblock_sequence: I32_MAX,
|
||||
microblock_hash: '',
|
||||
parent_index_block_hash: dbBlock2.parent_index_block_hash,
|
||||
parent_block_hash: dbBlock2.parent_block_hash,
|
||||
post_conditions: '0x01f5',
|
||||
fee_rate: 1234n,
|
||||
sponsored: false,
|
||||
sponsor_address: undefined,
|
||||
sender_address: addr2,
|
||||
origin_hash_mode: 1,
|
||||
event_count: 1,
|
||||
execution_cost_read_count: 0,
|
||||
execution_cost_read_length: 0,
|
||||
execution_cost_runtime: 0,
|
||||
execution_cost_write_count: 0,
|
||||
execution_cost_write_length: 0,
|
||||
};
|
||||
const nftEvent2: DbNftEvent = {
|
||||
canonical: true,
|
||||
event_type: DbEventTypeId.NonFungibleTokenAsset,
|
||||
asset_event_type_id: DbAssetEventTypeId.Transfer,
|
||||
event_index: 1,
|
||||
tx_id: stxTx1.tx_id,
|
||||
tx_index: 2,
|
||||
block_height: dbBlock2.block_height,
|
||||
asset_identifier: 'some-asset',
|
||||
value: '0x0000000000000000000000000000000000',
|
||||
recipient: addr2,
|
||||
sender: 'none',
|
||||
};
|
||||
await db.update({
|
||||
block: dbBlock2,
|
||||
microblocks: [],
|
||||
minerRewards: [],
|
||||
txs: [
|
||||
{
|
||||
tx: stxTx1,
|
||||
stxLockEvents: [],
|
||||
stxEvents: [],
|
||||
ftEvents: [],
|
||||
nftEvents: [nftEvent2],
|
||||
contractLogEvents: [],
|
||||
smartContracts: [],
|
||||
names: [],
|
||||
namespaces: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const result1 = await supertest(api.server).get(`/extended/v1/address/${addr2}/nft_events`);
|
||||
expect(result1.status).toBe(200);
|
||||
expect(result1.type).toBe('application/json');
|
||||
expect(result1.body.total).toEqual(1);
|
||||
expect(result1.body.nft_events.length).toEqual(1);
|
||||
expect(result1.body.nft_events[0].recipient).toBe(addr2);
|
||||
expect(result1.body.nft_events[0].tx_id).toBe(
|
||||
'0x1111100000000000000000000000000000000000000000000000000000000001'
|
||||
);
|
||||
expect(result1.body.nft_events[0].block_height).toBe(2);
|
||||
expect(result.body.nft_events[0].value.repr).toBe('0');
|
||||
|
||||
//check ownership for addr
|
||||
const result2 = await supertest(api.server).get(`/extended/v1/address/${addr1}/nft_events`);
|
||||
expect(result2.status).toBe(200);
|
||||
expect(result2.type).toBe('application/json');
|
||||
expect(result2.body.nft_events.length).toEqual(0);
|
||||
expect(result2.body.total).toEqual(0);
|
||||
});
|
||||
|
||||
test('nft invalid address', async () => {
|
||||
const result = await supertest(api.server).get(
|
||||
`/extended/v1/address/invalid-address/nft_events`
|
||||
);
|
||||
expect(result.status).toBe(400);
|
||||
expect(result.type).toBe('application/json');
|
||||
});
|
||||
|
||||
test('/transactions materialized view separates anchored and unanchored counts correctly', async () => {
|
||||
const contractId = 'SP3D6PV2ACBPEKYJTCMH7HEN02KP87QSP8KTEH335.megapont-ape-club-nft';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user