-
Notifications
You must be signed in to change notification settings - Fork 4
chore: bump stellar SDK to 16.0.1 #148
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
Open
Julink-eth
wants to merge
9
commits into
main
Choose a base branch
from
chore/bump-stellar-sdk
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
5e45f9e
chore: bump stellar SDK to 16.0.1
Julink-eth 85fa299
Merge branch 'main' into chore/bump-stellar-sdk
Julink-eth 2262735
chore: make the migration deeper and remove shim files
Julink-eth 2ad9c40
Potential fix for pull request finding
Julink-eth 57df779
Potential fix for pull request finding
Julink-eth caae06d
Potential fix for pull request finding
Julink-eth dcbf08b
Potential fix for pull request finding
Julink-eth 0715662
Merge branch 'main' into chore/bump-stellar-sdk
Julink-eth c189807
chore: fix recent changes from copilot + added tests
Julink-eth File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
126 changes: 126 additions & 0 deletions
126
packages/snap/src/services/network/HorizonClient.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| /* eslint-disable @typescript-eslint/naming-convention -- Horizon wire fields use snake_case */ | ||
| import { HorizonClient, HorizonNotFoundError } from './HorizonClient'; | ||
|
|
||
| describe('HorizonClient', () => { | ||
| const originalFetch = globalThis.fetch; | ||
| const fetchMock = jest.fn< | ||
| ReturnType<typeof fetch>, | ||
| Parameters<typeof fetch> | ||
| >(); | ||
|
|
||
| beforeEach(() => { | ||
| fetchMock.mockReset(); | ||
| globalThis.fetch = fetchMock; | ||
| }); | ||
|
|
||
| afterAll(() => { | ||
| globalThis.fetch = originalFetch; | ||
| }); | ||
|
|
||
| it('fetches the base fee from Horizon fee stats', async () => { | ||
| fetchMock.mockResolvedValue(jsonResponse({ last_ledger_base_fee: '123' })); | ||
| const client = new HorizonClient('https://horizon.example'); | ||
|
|
||
| const result = await client.fetchBaseFee(); | ||
|
|
||
| expect(result).toBe(123); | ||
| expect(fetchMock).toHaveBeenCalledWith( | ||
| 'https://horizon.example/fee_stats', | ||
| { | ||
| method: 'GET', | ||
| headers: { Accept: 'application/json' }, | ||
| }, | ||
| ); | ||
| }); | ||
|
|
||
| it('loads account responses with SDK-compatible account helpers', async () => { | ||
| fetchMock.mockResolvedValue( | ||
| jsonResponse({ | ||
| account_id: 'GB5QOHJZ6RACA26NFDIEHD7I7SLROLC5P4NATSG43OJV2C5WUR4VEUKG', | ||
| sequence: '42', | ||
| balances: [], | ||
| }), | ||
| ); | ||
| const client = new HorizonClient('https://horizon.example'); | ||
|
|
||
| const result = await client.loadAccount( | ||
| 'GB5QOHJZ6RACA26NFDIEHD7I7SLROLC5P4NATSG43OJV2C5WUR4VEUKG', | ||
| ); | ||
|
|
||
| expect(result.accountId()).toBe( | ||
| 'GB5QOHJZ6RACA26NFDIEHD7I7SLROLC5P4NATSG43OJV2C5WUR4VEUKG', | ||
| ); | ||
| expect(result.sequenceNumber()).toBe('42'); | ||
| }); | ||
|
|
||
| it('reads asset records from Horizon embedded collection responses', async () => { | ||
| const assetRecord = { | ||
| asset_code: 'USDC', | ||
| asset_issuer: 'GB5QOHJZ6RACA26NFDIEHD7I7SLROLC5P4NATSG43OJV2C5WUR4VEUKG', | ||
| }; | ||
| fetchMock.mockResolvedValue( | ||
| jsonResponse({ | ||
| _embedded: { | ||
| records: [assetRecord], | ||
| }, | ||
| }), | ||
| ); | ||
| const client = new HorizonClient('https://horizon.example'); | ||
|
|
||
| const result = await client.getAssetRecords({ | ||
| assetCode: 'USDC', | ||
| assetIssuer: 'GB5QOHJZ6RACA26NFDIEHD7I7SLROLC5P4NATSG43OJV2C5WUR4VEUKG', | ||
| }); | ||
|
|
||
| expect(result.records).toStrictEqual([assetRecord]); | ||
| }); | ||
|
|
||
| it('reads transaction records from Horizon embedded collection responses', async () => { | ||
| const transactionRecord = { | ||
| hash: 'transaction-hash', | ||
| source_account: | ||
| 'GB5QOHJZ6RACA26NFDIEHD7I7SLROLC5P4NATSG43OJV2C5WUR4VEUKG', | ||
| }; | ||
| fetchMock.mockResolvedValue( | ||
| jsonResponse({ | ||
| _links: { | ||
| next: { | ||
| href: '', | ||
| }, | ||
| }, | ||
| _embedded: { | ||
| records: [transactionRecord], | ||
| }, | ||
| }), | ||
| ); | ||
| const client = new HorizonClient('https://horizon.example'); | ||
|
|
||
| const result = await client.getTransactions({ | ||
| accountAddress: | ||
| 'GB5QOHJZ6RACA26NFDIEHD7I7SLROLC5P4NATSG43OJV2C5WUR4VEUKG', | ||
| cursor: '', | ||
| includeFailed: false, | ||
| limit: 10, | ||
| order: 'desc', | ||
| }); | ||
|
|
||
| expect(result.records).toStrictEqual([transactionRecord]); | ||
| }); | ||
|
|
||
| it('throws HorizonNotFoundError for 404 responses', async () => { | ||
| fetchMock.mockResolvedValue(jsonResponse({ title: 'Not Found' }, 404)); | ||
| const client = new HorizonClient('https://horizon.example'); | ||
|
|
||
| await expect(client.getTransaction('abc')).rejects.toThrow( | ||
| HorizonNotFoundError, | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| function jsonResponse(body: unknown, status: number = 200): Response { | ||
| return { | ||
| ok: status >= 200 && status < 300, | ||
| status, | ||
| text: async () => JSON.stringify(body), | ||
| } as Response; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,221 @@ | ||
| /* eslint-disable @typescript-eslint/naming-convention -- Horizon wire fields use snake_case */ | ||
| import type { Horizon } from '@stellar/stellar-sdk'; | ||
|
|
||
| type HorizonAccountJson = Horizon.AccountResponse & { | ||
| account_id?: string; | ||
| id?: string; | ||
| sequence?: string; | ||
| }; | ||
|
|
||
| type HorizonCollectionResponse<TRecord> = { | ||
| records?: TRecord[]; | ||
| _embedded?: { records?: TRecord[] }; | ||
| _links?: { | ||
| next?: { | ||
| href?: string; | ||
| }; | ||
| }; | ||
| }; | ||
|
|
||
| export type HorizonAssetRecord = { | ||
| asset_code?: string; | ||
| asset_issuer?: string; | ||
| }; | ||
|
|
||
| export type HorizonAssetRecordsResponse = { | ||
| records: HorizonAssetRecord[]; | ||
| }; | ||
|
|
||
| export type HorizonTransactionPage = { | ||
| records: Horizon.ServerApi.TransactionRecord[]; | ||
| next: () => Promise<HorizonTransactionPage>; | ||
| }; | ||
|
|
||
| /** | ||
| * Error thrown when Horizon returns HTTP 404. | ||
| */ | ||
| export class HorizonNotFoundError extends Error { | ||
| readonly status = 404; | ||
|
|
||
| constructor(message: string, options?: ErrorOptions) { | ||
| super(message, options); | ||
| this.name = 'HorizonNotFoundError'; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Small Snap-safe Horizon client using the platform `fetch` endowment directly. | ||
| */ | ||
| export class HorizonClient { | ||
| readonly #baseUrl: string; | ||
|
|
||
| constructor(baseUrl: string) { | ||
| this.#baseUrl = baseUrl.replace(/\/$/u, ''); | ||
| } | ||
|
|
||
| async fetchBaseFee(): Promise<number> { | ||
| const feeStats = await this.#requestJson<{ | ||
| last_ledger_base_fee?: string; | ||
| }>('/fee_stats'); | ||
| return parseInt(feeStats.last_ledger_base_fee ?? '', 10) || 100; | ||
| } | ||
|
|
||
| async loadAccount(accountAddress: string): Promise<Horizon.AccountResponse> { | ||
| const account = await this.#requestJson<HorizonAccountJson>( | ||
| `/accounts/${encodeURIComponent(accountAddress)}`, | ||
| ); | ||
| return this.#toAccountResponse(account); | ||
| } | ||
|
|
||
| async getAssetRecords(params: { | ||
| assetCode: string; | ||
| assetIssuer: string; | ||
| }): Promise<HorizonAssetRecordsResponse> { | ||
| const { assetCode, assetIssuer } = params; | ||
| const response = await this.#requestJson< | ||
| HorizonCollectionResponse<HorizonAssetRecord> | ||
| >( | ||
| `/assets?${encodeQuery({ | ||
| asset_code: assetCode, | ||
| asset_issuer: assetIssuer, | ||
| })}`, | ||
| ); | ||
|
|
||
| return { | ||
| records: response.records ?? response._embedded?.records ?? [], | ||
| }; | ||
| } | ||
|
Copilot marked this conversation as resolved.
|
||
|
|
||
| async getTransaction( | ||
| transactionHash: string, | ||
| ): Promise<Horizon.ServerApi.TransactionRecord> { | ||
| return this.#requestJson( | ||
| `/transactions/${encodeURIComponent(transactionHash)}`, | ||
| ); | ||
| } | ||
|
|
||
| async getTransactions(params: { | ||
| accountAddress: string; | ||
| cursor: string; | ||
| includeFailed: boolean; | ||
| limit: number; | ||
| order: 'asc' | 'desc'; | ||
| }): Promise<HorizonTransactionPage> { | ||
| const { accountAddress, cursor, includeFailed, limit, order } = params; | ||
| return this.#getTransactionPage( | ||
| `/accounts/${encodeURIComponent(accountAddress)}/transactions?${encodeQuery( | ||
| { | ||
| cursor, | ||
| include_failed: includeFailed, | ||
| limit, | ||
| order, | ||
| }, | ||
| )}`, | ||
| ); | ||
| } | ||
|
|
||
| async #getTransactionPage(url: string): Promise<HorizonTransactionPage> { | ||
| const response = | ||
| await this.#requestJson< | ||
| HorizonCollectionResponse<Horizon.ServerApi.TransactionRecord> | ||
| >(url); | ||
| const nextUrl = response._links?.next?.href; | ||
|
|
||
| const records = response.records ?? response._embedded?.records ?? []; | ||
|
|
||
| return { | ||
| records, | ||
| next: async (): Promise<HorizonTransactionPage> => { | ||
| if (nextUrl === undefined || nextUrl.length === 0) { | ||
| return emptyTransactionPage(); | ||
| } | ||
| return this.#getTransactionPage(nextUrl); | ||
| }, | ||
| }; | ||
| } | ||
|
Copilot marked this conversation as resolved.
|
||
|
|
||
| #toAccountResponse(account: HorizonAccountJson): Horizon.AccountResponse { | ||
| const accountId = account.account_id ?? account.id; | ||
| const { sequence } = account; | ||
|
|
||
| return Object.assign(account, { | ||
| accountId(): string | undefined { | ||
| return accountId; | ||
| }, | ||
| sequenceNumber(): string | undefined { | ||
| return sequence; | ||
| }, | ||
| }) as Horizon.AccountResponse; | ||
| } | ||
|
|
||
| async #requestJson<TResponse>(pathOrUrl: string): Promise<TResponse> { | ||
| const url = pathOrUrl.startsWith('http') | ||
| ? pathOrUrl | ||
| : `${this.#baseUrl}${pathOrUrl.startsWith('/') ? '' : '/'}${pathOrUrl}`; | ||
| const response = await fetch(url, { | ||
| method: 'GET', | ||
| headers: { | ||
| Accept: 'application/json', | ||
| }, | ||
| }); | ||
| const body = await response.text(); | ||
| const data = parseJsonBody(body); | ||
|
|
||
| if (response.status === 404) { | ||
| throw new HorizonNotFoundError(`Horizon resource not found: ${url}`, { | ||
| cause: data, | ||
| }); | ||
| } | ||
|
|
||
| if (!response.ok) { | ||
| throw new Error(`Horizon request failed with status ${response.status}`, { | ||
| cause: data, | ||
| }); | ||
| } | ||
|
|
||
| return data as TResponse; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Encodes query parameters without relying on URLSearchParams, which is not guaranteed in SES. | ||
| * | ||
| * @param params - Query parameters. | ||
| * @returns Encoded query string. | ||
| */ | ||
| function encodeQuery( | ||
| params: Record<string, boolean | number | string | undefined>, | ||
| ): string { | ||
| return Object.entries(params) | ||
| .filter(([, value]) => value !== undefined) | ||
| .map( | ||
| ([key, value]) => | ||
| `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`, | ||
| ) | ||
| .join('&'); | ||
| } | ||
|
|
||
| /** | ||
| * Parses a JSON response body. | ||
| * | ||
| * @param body - Response text. | ||
| * @returns Parsed JSON, or null for empty responses. | ||
| */ | ||
| function parseJsonBody(body: string): unknown { | ||
| if (body.length === 0) { | ||
| return null; | ||
| } | ||
| return JSON.parse(body); | ||
| } | ||
|
|
||
| /** | ||
| * Builds an empty Horizon transaction page. | ||
| * | ||
| * @returns Empty transaction page. | ||
| */ | ||
| function emptyTransactionPage(): HorizonTransactionPage { | ||
| return { | ||
| records: [], | ||
| next: async () => emptyTransactionPage(), | ||
| }; | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.