0g-orbit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/dist/cli/cli.d.ts +3 -0
  2. package/dist/cli/cli.d.ts.map +1 -0
  3. package/dist/cli/cli.js +59 -0
  4. package/dist/cli/cli.js.map +1 -0
  5. package/dist/cli/commands/account.d.ts +6 -0
  6. package/dist/cli/commands/account.d.ts.map +1 -0
  7. package/dist/cli/commands/account.js +23 -0
  8. package/dist/cli/commands/account.js.map +1 -0
  9. package/dist/cli/commands/inference.d.ts +15 -0
  10. package/dist/cli/commands/inference.d.ts.map +1 -0
  11. package/dist/cli/commands/inference.js +70 -0
  12. package/dist/cli/commands/inference.js.map +1 -0
  13. package/dist/cli/commands/init.d.ts +7 -0
  14. package/dist/cli/commands/init.d.ts.map +1 -0
  15. package/dist/cli/commands/init.js +60 -0
  16. package/dist/cli/commands/init.js.map +1 -0
  17. package/dist/cli/commands/storage.d.ts +19 -0
  18. package/dist/cli/commands/storage.d.ts.map +1 -0
  19. package/dist/cli/commands/storage.js +62 -0
  20. package/dist/cli/commands/storage.js.map +1 -0
  21. package/dist/cli/utils.d.ts +4 -0
  22. package/dist/cli/utils.d.ts.map +1 -0
  23. package/dist/cli/utils.js +20 -0
  24. package/dist/cli/utils.js.map +1 -0
  25. package/dist/errors.d.ts +26 -0
  26. package/dist/errors.d.ts.map +1 -0
  27. package/dist/errors.js +51 -0
  28. package/dist/errors.js.map +1 -0
  29. package/dist/index.d.ts +10 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +11 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/inference.d.ts +17 -0
  34. package/dist/inference.d.ts.map +1 -0
  35. package/dist/inference.js +179 -0
  36. package/dist/inference.js.map +1 -0
  37. package/dist/networks.d.ts +16 -0
  38. package/dist/networks.d.ts.map +1 -0
  39. package/dist/networks.js +48 -0
  40. package/dist/networks.js.map +1 -0
  41. package/dist/orbit.d.ts +27 -0
  42. package/dist/orbit.d.ts.map +1 -0
  43. package/dist/orbit.js +108 -0
  44. package/dist/orbit.js.map +1 -0
  45. package/dist/retry.d.ts +23 -0
  46. package/dist/retry.d.ts.map +1 -0
  47. package/dist/retry.js +90 -0
  48. package/dist/retry.js.map +1 -0
  49. package/dist/storage.d.ts +26 -0
  50. package/dist/storage.d.ts.map +1 -0
  51. package/dist/storage.js +121 -0
  52. package/dist/storage.js.map +1 -0
  53. package/dist/types.d.ts +81 -0
  54. package/dist/types.d.ts.map +1 -0
  55. package/dist/types.js +2 -0
  56. package/dist/types.js.map +1 -0
  57. package/examples/ai-chatbot/index.ts +74 -0
  58. package/examples/model-registry/index.ts +137 -0
  59. package/examples/quick-start/index.ts +65 -0
  60. package/package.json +42 -0
  61. package/packages/cli/package.json +30 -0
  62. package/packages/cli/src/cli.ts +69 -0
  63. package/packages/cli/src/commands/account.ts +29 -0
  64. package/packages/cli/src/commands/inference.ts +103 -0
  65. package/packages/cli/src/commands/init.ts +71 -0
  66. package/packages/cli/src/commands/storage.ts +91 -0
  67. package/packages/cli/src/utils.ts +21 -0
  68. package/packages/cli/tsconfig.json +8 -0
  69. package/packages/core/package.json +35 -0
  70. package/packages/core/src/errors.test.ts +99 -0
  71. package/packages/core/src/errors.ts +79 -0
  72. package/packages/core/src/index.ts +37 -0
  73. package/packages/core/src/inference.ts +256 -0
  74. package/packages/core/src/networks.test.ts +62 -0
  75. package/packages/core/src/networks.ts +62 -0
  76. package/packages/core/src/orbit.test.ts +153 -0
  77. package/packages/core/src/orbit.ts +159 -0
  78. package/packages/core/src/retry.test.ts +99 -0
  79. package/packages/core/src/retry.ts +99 -0
  80. package/packages/core/src/storage.test.ts +199 -0
  81. package/packages/core/src/storage.ts +158 -0
  82. package/packages/core/src/types.ts +85 -0
  83. package/packages/core/tsconfig.json +8 -0
  84. package/packages/core/vitest.config.ts +7 -0
  85. package/src/cli/cli.ts +69 -0
  86. package/src/cli/commands/account.ts +29 -0
  87. package/src/cli/commands/inference.ts +103 -0
  88. package/src/cli/commands/init.ts +71 -0
  89. package/src/cli/commands/storage.ts +91 -0
  90. package/src/cli/utils.ts +21 -0
  91. package/src/errors.test.ts +99 -0
  92. package/src/errors.ts +79 -0
  93. package/src/index.ts +37 -0
  94. package/src/inference.ts +256 -0
  95. package/src/networks.test.ts +62 -0
  96. package/src/networks.ts +62 -0
  97. package/src/orbit.test.ts +153 -0
  98. package/src/orbit.ts +159 -0
  99. package/src/retry.test.ts +99 -0
  100. package/src/retry.ts +99 -0
  101. package/src/storage.test.ts +199 -0
  102. package/src/storage.ts +158 -0
  103. package/src/types.ts +85 -0
  104. package/tsconfig.json +20 -0
  105. package/vitest.config.ts +7 -0
@@ -0,0 +1,99 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import { withRetry, isTransientError } from './retry.js'
3
+
4
+ describe('withRetry', () => {
5
+ it('returns on first success', async () => {
6
+ const fn = vi.fn().mockResolvedValue('ok')
7
+ const result = await withRetry(fn)
8
+ expect(result).toBe('ok')
9
+ expect(fn).toHaveBeenCalledTimes(1)
10
+ })
11
+
12
+ it('retries on transient error and succeeds', async () => {
13
+ const fn = vi.fn()
14
+ .mockRejectedValueOnce(new Error('ECONNREFUSED'))
15
+ .mockResolvedValue('ok')
16
+
17
+ const result = await withRetry(fn, { baseDelay: 10 })
18
+ expect(result).toBe('ok')
19
+ expect(fn).toHaveBeenCalledTimes(2)
20
+ })
21
+
22
+ it('throws after maxAttempts exhausted', async () => {
23
+ const fn = vi.fn().mockRejectedValue(new Error('ETIMEDOUT'))
24
+
25
+ await expect(
26
+ withRetry(fn, { maxAttempts: 3, baseDelay: 10 })
27
+ ).rejects.toThrow('ETIMEDOUT')
28
+ expect(fn).toHaveBeenCalledTimes(3)
29
+ })
30
+
31
+ it('does not retry non-transient errors', async () => {
32
+ const fn = vi.fn().mockRejectedValue(new Error('Invalid private key'))
33
+
34
+ await expect(
35
+ withRetry(fn, { maxAttempts: 3, baseDelay: 10 })
36
+ ).rejects.toThrow('Invalid private key')
37
+ expect(fn).toHaveBeenCalledTimes(1)
38
+ })
39
+
40
+ it('calls onRetry callback', async () => {
41
+ const onRetry = vi.fn()
42
+ const fn = vi.fn()
43
+ .mockRejectedValueOnce(new Error('ECONNRESET'))
44
+ .mockResolvedValue('ok')
45
+
46
+ await withRetry(fn, { baseDelay: 10, onRetry })
47
+ expect(onRetry).toHaveBeenCalledTimes(1)
48
+ expect(onRetry).toHaveBeenCalledWith(1, expect.any(Error))
49
+ })
50
+
51
+ it('respects custom isRetryable predicate', async () => {
52
+ const fn = vi.fn()
53
+ .mockRejectedValueOnce(new Error('custom-retryable'))
54
+ .mockResolvedValue('ok')
55
+
56
+ const result = await withRetry(fn, {
57
+ baseDelay: 10,
58
+ isRetryable: (err) => err instanceof Error && err.message === 'custom-retryable',
59
+ })
60
+ expect(result).toBe('ok')
61
+ expect(fn).toHaveBeenCalledTimes(2)
62
+ })
63
+ })
64
+
65
+ describe('isTransientError', () => {
66
+ it.each([
67
+ 'ECONNREFUSED',
68
+ 'ECONNRESET',
69
+ 'ETIMEDOUT',
70
+ 'ENOTFOUND',
71
+ 'fetch failed',
72
+ 'network error',
73
+ 'timeout exceeded',
74
+ 'server error',
75
+ 'bad gateway',
76
+ 'service unavailable',
77
+ 'rate limit exceeded',
78
+ 'missing response',
79
+ 'connection error',
80
+ 'could not detect network',
81
+ ])('returns true for "%s"', (msg) => {
82
+ expect(isTransientError(new Error(msg))).toBe(true)
83
+ })
84
+
85
+ it.each([
86
+ 'Invalid private key',
87
+ 'insufficient funds for gas',
88
+ 'File not found',
89
+ 'unknown error',
90
+ ])('returns false for "%s"', (msg) => {
91
+ expect(isTransientError(new Error(msg))).toBe(false)
92
+ })
93
+
94
+ it('returns false for non-Error values', () => {
95
+ expect(isTransientError('string error')).toBe(false)
96
+ expect(isTransientError(null)).toBe(false)
97
+ expect(isTransientError(42)).toBe(false)
98
+ })
99
+ })
package/src/retry.ts ADDED
@@ -0,0 +1,99 @@
1
+ export interface RetryOptions {
2
+ /** Maximum number of attempts (default: 3) */
3
+ maxAttempts?: number
4
+ /** Base delay between retries in ms (default: 1000). Doubles each retry. */
5
+ baseDelay?: number
6
+ /** Maximum delay cap in ms (default: 10000) */
7
+ maxDelay?: number
8
+ /** Predicate to decide if an error is retryable (default: transient errors only) */
9
+ isRetryable?: (err: unknown) => boolean
10
+ /** Called before each retry with attempt number and error */
11
+ onRetry?: (attempt: number, err: unknown) => void
12
+ }
13
+
14
+ const DEFAULT_OPTIONS: Required<Omit<RetryOptions, 'onRetry'>> = {
15
+ maxAttempts: 3,
16
+ baseDelay: 1000,
17
+ maxDelay: 10_000,
18
+ isRetryable: isTransientError,
19
+ }
20
+
21
+ /**
22
+ * Retry an async operation with exponential backoff.
23
+ * Only retries on transient errors (network, timeout, 5xx) by default.
24
+ */
25
+ export async function withRetry<T>(
26
+ fn: () => Promise<T>,
27
+ options: RetryOptions = {}
28
+ ): Promise<T> {
29
+ const opts = { ...DEFAULT_OPTIONS, ...options }
30
+ let lastError: unknown
31
+
32
+ for (let attempt = 1; attempt <= opts.maxAttempts; attempt++) {
33
+ try {
34
+ return await fn()
35
+ } catch (err) {
36
+ lastError = err
37
+
38
+ if (attempt === opts.maxAttempts || !opts.isRetryable(err)) {
39
+ throw err
40
+ }
41
+
42
+ if (options.onRetry) {
43
+ options.onRetry(attempt, err)
44
+ }
45
+
46
+ // Exponential backoff with jitter
47
+ const delay = Math.min(
48
+ opts.baseDelay * Math.pow(2, attempt - 1) + Math.random() * 500,
49
+ opts.maxDelay
50
+ )
51
+ await sleep(delay)
52
+ }
53
+ }
54
+
55
+ throw lastError
56
+ }
57
+
58
+ function sleep(ms: number): Promise<void> {
59
+ return new Promise((resolve) => setTimeout(resolve, ms))
60
+ }
61
+
62
+ /**
63
+ * Determines if an error is transient and worth retrying.
64
+ * Covers: network failures, timeouts, RPC errors, 5xx responses.
65
+ */
66
+ export function isTransientError(err: unknown): boolean {
67
+ if (!(err instanceof Error)) return false
68
+ const msg = err.message.toLowerCase()
69
+
70
+ // Network-level failures
71
+ if (msg.includes('econnrefused')) return true
72
+ if (msg.includes('econnreset')) return true
73
+ if (msg.includes('etimedout')) return true
74
+ if (msg.includes('enotfound')) return true
75
+ if (msg.includes('epipe')) return true
76
+ if (msg.includes('fetch failed')) return true
77
+ if (msg.includes('network error')) return true
78
+
79
+ // Timeout
80
+ if (msg.includes('timeout')) return true
81
+ if (err.name === 'AbortError') return true
82
+
83
+ // RPC-level transient errors
84
+ if (msg.includes('server error')) return true
85
+ if (msg.includes('bad gateway')) return true
86
+ if (msg.includes('service unavailable')) return true
87
+ if (msg.includes('rate limit')) return true
88
+ if (msg.includes('429')) return true
89
+ if (msg.includes('502')) return true
90
+ if (msg.includes('503')) return true
91
+ if (msg.includes('504')) return true
92
+
93
+ // ethers provider errors
94
+ if (msg.includes('missing response')) return true
95
+ if (msg.includes('connection error')) return true
96
+ if (msg.includes('could not detect network')) return true
97
+
98
+ return false
99
+ }
@@ -0,0 +1,199 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
+ import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs'
3
+ import { tmpdir } from 'node:os'
4
+ import { join } from 'node:path'
5
+
6
+ // Mock the 0G SDK
7
+ vi.mock('@0gfoundation/0g-ts-sdk', () => {
8
+ const mockClose = vi.fn()
9
+ const mockMerkleTree = vi.fn().mockResolvedValue([{ rootHash: '0xabc' }, null])
10
+
11
+ return {
12
+ ZgFile: {
13
+ fromFilePath: vi.fn().mockResolvedValue({
14
+ merkleTree: mockMerkleTree,
15
+ close: mockClose,
16
+ size: () => 100,
17
+ }),
18
+ },
19
+ Indexer: vi.fn().mockImplementation(() => ({
20
+ upload: vi.fn().mockResolvedValue([
21
+ { txHash: '0xtx123', rootHash: '0xroot456' },
22
+ null,
23
+ ]),
24
+ download: vi.fn().mockResolvedValue(null),
25
+ })),
26
+ }
27
+ })
28
+
29
+ import { StorageClient } from './storage.js'
30
+ import { StorageError } from './errors.js'
31
+ import { NETWORKS } from './networks.js'
32
+ import { Indexer, ZgFile } from '@0gfoundation/0g-ts-sdk'
33
+
34
+ const mockSigner = { address: '0x123' } as any
35
+ const testNetwork = { ...NETWORKS.testnet }
36
+
37
+ describe('StorageClient', () => {
38
+ beforeEach(() => {
39
+ vi.clearAllMocks()
40
+ })
41
+
42
+ describe('store', () => {
43
+ it('uploads a file and returns root + tx hash', async () => {
44
+ const client = new StorageClient(testNetwork, mockSigner)
45
+ const result = await client.store('/tmp/test.txt')
46
+ expect(result.root).toBe('0xroot456')
47
+ expect(result.txHash).toBe('0xtx123')
48
+ })
49
+
50
+ it('passes tags and replicas to upload', async () => {
51
+ const client = new StorageClient(testNetwork, mockSigner)
52
+ await client.store('/tmp/test.txt', { tags: '0xbeef', replicas: 3 })
53
+
54
+ const indexerInstance = (Indexer as any).mock.results[0].value
55
+ expect(indexerInstance.upload).toHaveBeenCalledWith(
56
+ expect.anything(),
57
+ testNetwork.rpcUrl,
58
+ expect.anything(),
59
+ expect.objectContaining({ tags: '0xbeef', expectedReplica: 3 }),
60
+ expect.objectContaining({ Retries: 5, Interval: 3 })
61
+ )
62
+ })
63
+
64
+ it('always passes gas retry options', async () => {
65
+ const client = new StorageClient(testNetwork, mockSigner)
66
+ await client.store('/tmp/test.txt')
67
+
68
+ const indexerInstance = (Indexer as any).mock.results[0].value
69
+ const retryOpts = indexerInstance.upload.mock.calls[0][4]
70
+ expect(retryOpts).toBeDefined()
71
+ expect(retryOpts.Retries).toBe(5)
72
+ expect(retryOpts.MaxGasPrice).toBeGreaterThan(0)
73
+ })
74
+
75
+ it('throws StorageError with suggestion on merkle failure', async () => {
76
+ const mockFile = {
77
+ merkleTree: vi.fn().mockResolvedValue([null, new Error('corrupt file')]),
78
+ close: vi.fn(),
79
+ }
80
+ ;(ZgFile.fromFilePath as any).mockResolvedValueOnce(mockFile)
81
+
82
+ const client = new StorageClient(testNetwork, mockSigner)
83
+ try {
84
+ await client.store('/tmp/bad.txt')
85
+ expect.unreachable('should have thrown')
86
+ } catch (err) {
87
+ expect(err).toBeInstanceOf(StorageError)
88
+ expect((err as StorageError).suggestion).toContain('file exists')
89
+ }
90
+ })
91
+
92
+ it('throws StorageError with gas suggestion on underpriced', async () => {
93
+ const indexerMock = {
94
+ upload: vi.fn().mockResolvedValue([
95
+ { txHash: '', rootHash: '' },
96
+ new Error('transaction underpriced'),
97
+ ]),
98
+ download: vi.fn(),
99
+ }
100
+ ;(Indexer as any).mockImplementationOnce(() => indexerMock)
101
+
102
+ const client = new StorageClient(testNetwork, mockSigner)
103
+ try {
104
+ await client.store('/tmp/test.txt')
105
+ expect.unreachable('should have thrown')
106
+ } catch (err) {
107
+ expect(err).toBeInstanceOf(StorageError)
108
+ expect((err as StorageError).suggestion).toContain('maxGasPrice')
109
+ }
110
+ })
111
+
112
+ it('always closes the file even on error', async () => {
113
+ const mockClose = vi.fn()
114
+ const mockFile = {
115
+ merkleTree: vi.fn().mockRejectedValue(new Error('boom')),
116
+ close: mockClose,
117
+ }
118
+ ;(ZgFile.fromFilePath as any).mockResolvedValueOnce(mockFile)
119
+
120
+ const client = new StorageClient(testNetwork, mockSigner)
121
+ await client.store('/tmp/test.txt').catch(() => {})
122
+ expect(mockClose).toHaveBeenCalled()
123
+ })
124
+ })
125
+
126
+ describe('storeData', () => {
127
+ it('stores a string by writing to temp file', async () => {
128
+ const client = new StorageClient(testNetwork, mockSigner)
129
+ const result = await client.storeData('hello world')
130
+ expect(result.root).toBe('0xroot456')
131
+ expect(result.txHash).toBe('0xtx123')
132
+
133
+ // Verify ZgFile.fromFilePath was called with a temp path
134
+ const calledPath = (ZgFile.fromFilePath as any).mock.calls[0][0]
135
+ expect(calledPath).toContain('orbit-')
136
+ })
137
+
138
+ it('stores a Buffer', async () => {
139
+ const client = new StorageClient(testNetwork, mockSigner)
140
+ const result = await client.storeData(Buffer.from('binary data'))
141
+ expect(result.root).toBe('0xroot456')
142
+ })
143
+
144
+ it('stores a Uint8Array', async () => {
145
+ const client = new StorageClient(testNetwork, mockSigner)
146
+ const result = await client.storeData(new Uint8Array([1, 2, 3]))
147
+ expect(result.root).toBe('0xroot456')
148
+ })
149
+
150
+ it('cleans up temp file after upload', async () => {
151
+ const client = new StorageClient(testNetwork, mockSigner)
152
+ await client.storeData('cleanup test')
153
+
154
+ const calledPath = (ZgFile.fromFilePath as any).mock.calls[0][0]
155
+ // Temp file should have been deleted
156
+ expect(existsSync(calledPath)).toBe(false)
157
+ })
158
+
159
+ it('cleans up temp file even on error', async () => {
160
+ const mockFile = {
161
+ merkleTree: vi.fn().mockResolvedValue([null, new Error('fail')]),
162
+ close: vi.fn(),
163
+ }
164
+ ;(ZgFile.fromFilePath as any).mockResolvedValueOnce(mockFile)
165
+
166
+ const client = new StorageClient(testNetwork, mockSigner)
167
+ await client.storeData('error cleanup test').catch(() => {})
168
+
169
+ const calledPath = (ZgFile.fromFilePath as any).mock.calls[0][0]
170
+ expect(existsSync(calledPath)).toBe(false)
171
+ })
172
+ })
173
+
174
+ describe('retrieve', () => {
175
+ it('downloads successfully', async () => {
176
+ const client = new StorageClient(testNetwork, mockSigner)
177
+ await expect(
178
+ client.retrieve('0xroot', '/tmp/output.txt')
179
+ ).resolves.toBeUndefined()
180
+ })
181
+
182
+ it('throws StorageError on download failure', async () => {
183
+ const indexerMock = {
184
+ upload: vi.fn(),
185
+ download: vi.fn().mockResolvedValue(new Error('file not found: 404')),
186
+ }
187
+ ;(Indexer as any).mockImplementationOnce(() => indexerMock)
188
+
189
+ const client = new StorageClient(testNetwork, mockSigner)
190
+ try {
191
+ await client.retrieve('0xbad', '/tmp/out.txt')
192
+ expect.unreachable('should have thrown')
193
+ } catch (err) {
194
+ expect(err).toBeInstanceOf(StorageError)
195
+ expect((err as StorageError).suggestion).toContain('root hash')
196
+ }
197
+ })
198
+ })
199
+ })
package/src/storage.ts ADDED
@@ -0,0 +1,158 @@
1
+ import { Indexer, ZgFile } from '@0gfoundation/0g-ts-sdk'
2
+ import type { Signer } from 'ethers'
3
+ import { writeFileSync, unlinkSync } from 'node:fs'
4
+ import { tmpdir } from 'node:os'
5
+ import { join } from 'node:path'
6
+ import { randomBytes } from 'node:crypto'
7
+ import type { NetworkConfig } from './networks.js'
8
+ import type { StoreResult, StoreOptions, RetrieveOptions } from './types.js'
9
+ import { StorageError } from './errors.js'
10
+ import { withRetry, isTransientError } from './retry.js'
11
+
12
+ const DEFAULT_REPLICAS = 1
13
+ const DEFAULT_MAX_GAS_PRICE = 25_000_000_000 // 25 gwei — generous ceiling for auto-escalation
14
+
15
+ export class StorageClient {
16
+ private indexer: Indexer
17
+ private rpcUrl: string
18
+ private signer: Signer
19
+
20
+ constructor(network: NetworkConfig, signer: Signer) {
21
+ this.indexer = new Indexer(network.indexerUrl)
22
+ this.rpcUrl = network.rpcUrl
23
+ this.signer = signer
24
+ }
25
+
26
+ /**
27
+ * Upload a file to 0G Storage by file path.
28
+ * Retries transient failures automatically. Gas escalates on each retry.
29
+ */
30
+ async store(filePath: string, options: StoreOptions = {}): Promise<StoreResult> {
31
+ return withRetry(
32
+ () => this._store(filePath, options),
33
+ {
34
+ maxAttempts: 3,
35
+ isRetryable: (err) => {
36
+ if (!(err instanceof StorageError)) return isTransientError(err)
37
+ const msg = err.message.toLowerCase()
38
+ return msg.includes('gas') ||
39
+ msg.includes('timeout') ||
40
+ msg.includes('etimedout') ||
41
+ msg.includes('econnrefused') ||
42
+ msg.includes('underpriced')
43
+ },
44
+ }
45
+ )
46
+ }
47
+
48
+ /**
49
+ * Upload raw data (string, Buffer, or Uint8Array) to 0G Storage.
50
+ * Writes to a temp file, uploads, then cleans up.
51
+ */
52
+ async storeData(
53
+ data: string | Buffer | Uint8Array,
54
+ options: StoreOptions = {}
55
+ ): Promise<StoreResult> {
56
+ const tempPath = join(tmpdir(), `orbit-${randomBytes(8).toString('hex')}`)
57
+ try {
58
+ writeFileSync(tempPath, data)
59
+ return await this.store(tempPath, options)
60
+ } finally {
61
+ try { unlinkSync(tempPath) } catch { /* ignore cleanup errors */ }
62
+ }
63
+ }
64
+
65
+ private async _store(filePath: string, options: StoreOptions): Promise<StoreResult> {
66
+ const file = await ZgFile.fromFilePath(filePath)
67
+ try {
68
+ const [tree, treeErr] = await file.merkleTree()
69
+ if (treeErr || !tree) {
70
+ throw new StorageError(
71
+ `Failed to compute merkle tree: ${treeErr?.message ?? 'unknown error'}`,
72
+ 'Ensure the file exists, is readable, and is not empty.'
73
+ )
74
+ }
75
+
76
+ // Always enable gas auto-escalation via the SDK's built-in retry.
77
+ // MaxGasPrice sets the ceiling — the SDK starts from the network estimate
78
+ // and bumps 10% on each retry until it hits this cap.
79
+ const maxGas = options.maxGasPrice
80
+ ? Number(options.maxGasPrice)
81
+ : DEFAULT_MAX_GAS_PRICE
82
+
83
+ // Cast signer to avoid ESM/CJS ethers type mismatch
84
+ const [result, uploadErr] = await this.indexer.upload(
85
+ file,
86
+ this.rpcUrl,
87
+ this.signer as any,
88
+ {
89
+ tags: options.tags ?? '0x',
90
+ expectedReplica: options.replicas ?? DEFAULT_REPLICAS,
91
+ },
92
+ { Retries: 5, Interval: 3, MaxGasPrice: maxGas }
93
+ )
94
+
95
+ if (uploadErr) {
96
+ const msg = uploadErr.message
97
+ let suggestion = 'Check your OG balance and try again.'
98
+ if (msg.includes('gas') || msg.includes('underpriced')) {
99
+ suggestion = `Gas auto-escalation hit the ceiling (${maxGas} wei). Try a higher maxGasPrice, e.g. { maxGasPrice: ${maxGas * 2}n }`
100
+ } else if (msg.includes('timeout') || msg.includes('ETIMEDOUT')) {
101
+ suggestion = 'The RPC endpoint timed out. Try a different RPC URL with the rpcUrl option.'
102
+ } else if (msg.includes('insufficient funds')) {
103
+ suggestion = 'Your wallet needs more OG for gas. Get testnet OG from: https://faucet.0g.ai'
104
+ }
105
+ throw new StorageError(`Upload failed: ${msg}`, suggestion)
106
+ }
107
+
108
+ if ('txHash' in result) {
109
+ return {
110
+ root: result.rootHash,
111
+ txHash: result.txHash,
112
+ }
113
+ }
114
+
115
+ // Fragmented upload — return the first root hash as primary handle
116
+ return {
117
+ root: result.rootHashes[0],
118
+ txHash: result.txHashes[0],
119
+ }
120
+ } finally {
121
+ await file.close()
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Download a file from 0G Storage by root hash.
127
+ */
128
+ async retrieve(
129
+ rootHash: string,
130
+ outputPath: string,
131
+ options: RetrieveOptions = {}
132
+ ): Promise<void> {
133
+ return withRetry(
134
+ () => this._retrieve(rootHash, outputPath, options),
135
+ { maxAttempts: 3 }
136
+ )
137
+ }
138
+
139
+ private async _retrieve(
140
+ rootHash: string,
141
+ outputPath: string,
142
+ options: RetrieveOptions
143
+ ): Promise<void> {
144
+ const err = await this.indexer.download(
145
+ rootHash,
146
+ outputPath,
147
+ options.proof ?? false
148
+ )
149
+ if (err) {
150
+ const msg = err.message
151
+ let suggestion = 'Verify the root hash is correct and the file was uploaded successfully.'
152
+ if (msg.includes('not found') || msg.includes('404')) {
153
+ suggestion = 'This root hash was not found on the network. Double-check the hash or ensure the upload completed.'
154
+ }
155
+ throw new StorageError(`Download failed: ${msg}`, suggestion)
156
+ }
157
+ }
158
+ }
package/src/types.ts ADDED
@@ -0,0 +1,85 @@
1
+ import type { NetworkName } from './networks.js'
2
+
3
+ export interface OrbitConfig {
4
+ /** Private key for signing transactions. Falls back to PRIVATE_KEY env var if omitted. */
5
+ privateKey?: string
6
+ /** Network to connect to */
7
+ network: NetworkName
8
+ /** Custom RPC URL (overrides network default) */
9
+ rpcUrl?: string
10
+ /** Custom indexer URL (overrides network default) */
11
+ indexerUrl?: string
12
+ }
13
+
14
+ export interface StoreResult {
15
+ /** Merkle root hash — use this to retrieve the file */
16
+ root: string
17
+ /** On-chain transaction hash */
18
+ txHash: string
19
+ }
20
+
21
+ export interface StoreOptions {
22
+ /** Custom tags for the upload (hex string) */
23
+ tags?: string
24
+ /** Expected number of replicas */
25
+ replicas?: number
26
+ /** Maximum gas price in wei */
27
+ maxGasPrice?: bigint
28
+ }
29
+
30
+ export interface RetrieveOptions {
31
+ /** Whether to verify merkle proofs during download */
32
+ proof?: boolean
33
+ }
34
+
35
+ export interface InferResult {
36
+ /** The model's response text */
37
+ content: string
38
+ /** The model used */
39
+ model: string
40
+ /** Token usage */
41
+ usage?: {
42
+ promptTokens: number
43
+ completionTokens: number
44
+ totalTokens: number
45
+ }
46
+ /** Whether the response passed TEE verification (null if not verifiable) */
47
+ verified: boolean | null
48
+ }
49
+
50
+ export interface InferOptions {
51
+ /** Messages for chat completion */
52
+ messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>
53
+ /** Temperature for sampling (0-2) */
54
+ temperature?: number
55
+ /** Maximum tokens to generate */
56
+ maxTokens?: number
57
+ /** Specific provider address (auto-selected if omitted) */
58
+ provider?: string
59
+ /** Request timeout in milliseconds (default: 30000) */
60
+ timeout?: number
61
+ }
62
+
63
+ export interface ServiceInfo {
64
+ /** Provider wallet address */
65
+ provider: string
66
+ /** Model name */
67
+ model: string
68
+ /** Service endpoint URL */
69
+ url: string
70
+ /** Input token price (neuron) */
71
+ inputPrice: bigint
72
+ /** Output token price (neuron) */
73
+ outputPrice: bigint
74
+ /** Whether TEE verification is available */
75
+ verifiable: boolean
76
+ }
77
+
78
+ export interface AccountStatus {
79
+ /** Main ledger balance in OG */
80
+ balance: string
81
+ /** Network name */
82
+ network: NetworkName
83
+ /** Wallet address */
84
+ address: string
85
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "lib": ["ES2022"],
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "resolveJsonModule": true,
12
+ "declaration": true,
13
+ "declarationMap": true,
14
+ "sourceMap": true,
15
+ "outDir": "./dist",
16
+ "rootDir": "./src"
17
+ },
18
+ "include": ["src/**/*.ts"],
19
+ "exclude": ["src/**/*.test.ts"]
20
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from 'vitest/config'
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ['src/**/*.test.ts'],
6
+ },
7
+ })