solana-terminator-skill 4.1.1
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.
- package/README.md +77 -0
- package/SKILL.md +123 -0
- package/install.js +74 -0
- package/package.json +53 -0
- package/solana-autonomy/SKILL.md +24 -0
- package/solana-autonomy.js +703 -0
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# 🤖 Solana Terminator Skill v4.0
|
|
2
|
+
|
|
3
|
+
> **Sovereign Solana Identity & Autonomous Survival Engine for Conway Automaton.**
|
|
4
|
+
|
|
5
|
+
This skill gives your agent "Solana Hands" and a sophisticated "Life Support" system. Optimized for reliability, speed (Jupiter/Tensor APIs), and security (local signing).
|
|
6
|
+
|
|
7
|
+
[](https://github.com/Lord14sol/solana-terminator-skill)
|
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 🌟 Key Features
|
|
13
|
+
|
|
14
|
+
- **Sovereign Identity**: Local wallet management (`~/.automaton/solana-wallet.json`).
|
|
15
|
+
- **Life Support**: Automatic SOL → USDC swaps via Jupiter when funds are low.
|
|
16
|
+
- **Deep DeFi Integration**: 18 methods across DEX, NFTs, Memecoins, and Liquidity.
|
|
17
|
+
- **Security First**: Private keys **never leave your machine**. Transactions are built and signed locally.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 🛠 Installation
|
|
22
|
+
|
|
23
|
+
The fastest way to install is via **npx**:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx solana-terminator-skill
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
This automates the directory creation, file downloads, and dependency setup.
|
|
30
|
+
|
|
31
|
+
### Manual Installation (Alternative)
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# 1. Create skill directory
|
|
35
|
+
mkdir -p ~/.automaton/skills/solana-terminator
|
|
36
|
+
|
|
37
|
+
# 2. Download the skill files to that folder
|
|
38
|
+
# (solana-autonomy.js, SKILL.md, package.json)
|
|
39
|
+
|
|
40
|
+
# 3. Install dependencies
|
|
41
|
+
npm install @solana/web3.js @solana/spl-token axios dotenv bs58
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 🚀 Capabilities (18 Methods)
|
|
47
|
+
|
|
48
|
+
| Category | Methods |
|
|
49
|
+
|----------|---------|
|
|
50
|
+
| **Identity** | `getAddress`, `getSolBalance`, `getUsdcBalance`, `getStatus` |
|
|
51
|
+
| **Survival** | `keepAlive`, `requestAirdrop` |
|
|
52
|
+
| **Transfers** | `sendSol`, `sendToken` |
|
|
53
|
+
| **DEX** | `swap` (Jupiter Aggregator v6) |
|
|
54
|
+
| **Memecoins** | `pumpBuy`, `pumpSell` (Pump.fun via PumpPortal) |
|
|
55
|
+
| **NFTs** | `buyNft`, `sellNft` (Tensor GraphQL) |
|
|
56
|
+
| **DeFi** | `stake`, `unstake`, `memo` |
|
|
57
|
+
| **Liquidity** | `addLiquidity`, `removeLiquidity` (Meteora DLMM) |
|
|
58
|
+
|
|
59
|
+
### Featured: `sellNft(mint, minPriceSol)`
|
|
60
|
+
Sells an NFT directly into a Tensor liquidity pool.
|
|
61
|
+
- **Safety**: Automatically rejects if the best pool bid is below your `minPriceSol`.
|
|
62
|
+
- **Fast**: Efficient GraphQL quoting and local signing.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## ⚙️ Configuration
|
|
67
|
+
|
|
68
|
+
Environment variables (optional):
|
|
69
|
+
- `SOLANA_RPC_URL`: Your custom RPC (defaults to Mainnet Beta).
|
|
70
|
+
- `JUPITER_API_KEY`: Required for high-frequency trading (optional for basic use).
|
|
71
|
+
- `TENSOR_API_KEY`: Required for NFT operations.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 📄 License
|
|
76
|
+
|
|
77
|
+
MIT © [Lord14sol](https://github.com/Lord14sol)
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: solana-autonomy
|
|
3
|
+
description: "Full Solana toolkit for AI agents. Identity, survival, Jupiter swaps, Pump.fun memecoins, SOL transfers, staking, on-chain memos, Tensor NFTs, and Meteora liquidity."
|
|
4
|
+
auto-activate: true
|
|
5
|
+
---
|
|
6
|
+
# Solana Hands — Full Solana Toolkit
|
|
7
|
+
|
|
8
|
+
You have a sovereign identity and full operational capabilities on Solana. Fees are ~$0.00025/tx.
|
|
9
|
+
|
|
10
|
+
## Your Identity
|
|
11
|
+
|
|
12
|
+
- Keypair at `~/.automaton/solana-wallet.json` (0o600 permissions)
|
|
13
|
+
- Mainnet-beta by default. Override: `SOLANA_RPC_URL`
|
|
14
|
+
|
|
15
|
+
```javascript
|
|
16
|
+
import { SolanaAutonomy } from '~/.automaton/skills/solana-terminator/solana-autonomy.js';
|
|
17
|
+
const solana = new SolanaAutonomy();
|
|
18
|
+
|
|
19
|
+
solana.getAddress() // Solana address (base58)
|
|
20
|
+
await solana.getSolBalance() // SOL balance
|
|
21
|
+
await solana.getUsdcBalance() // USDC balance on Solana
|
|
22
|
+
await solana.getStatus() // { address, sol, usdc, solLow, usdcLow }
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Survival Engine — keepAlive()
|
|
26
|
+
|
|
27
|
+
Call every ~10 min. No args. Auto-swaps SOL to USDC when low.
|
|
28
|
+
|
|
29
|
+
```javascript
|
|
30
|
+
const result = await solana.keepAlive();
|
|
31
|
+
// { success, status: 'nominal' | 'stabilized' | 'dead' | 'error' }
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## All Capabilities
|
|
35
|
+
|
|
36
|
+
### Send SOL
|
|
37
|
+
```javascript
|
|
38
|
+
await solana.sendSol('DestinationAddress...', 0.5); // Send 0.5 SOL
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Send Any SPL Token
|
|
42
|
+
```javascript
|
|
43
|
+
await solana.sendToken('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', 'Dest...', 1_000_000); // 1 USDC
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Jupiter Swap (Any Token)
|
|
47
|
+
```javascript
|
|
48
|
+
await solana.swap(
|
|
49
|
+
'So11111111111111111111111111111111111111112', // SOL
|
|
50
|
+
'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // USDC
|
|
51
|
+
10_000_000, // 0.01 SOL in lamports
|
|
52
|
+
50 // 0.5% slippage
|
|
53
|
+
);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Buy Memecoins on Pump.fun
|
|
57
|
+
```javascript
|
|
58
|
+
await solana.pumpBuy('TokenMintAddress...', 0.1); // Buy with 0.1 SOL
|
|
59
|
+
await solana.pumpSell('TokenMintAddress...', '100%'); // Sell all
|
|
60
|
+
await solana.pumpSell('TokenMintAddress...', 5000); // Sell 5000 tokens
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Stake SOL
|
|
64
|
+
```javascript
|
|
65
|
+
await solana.stake(1.0, 'ValidatorVoteAddress...');
|
|
66
|
+
await solana.unstake('StakeAccountAddress...');
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Write On-chain Memo
|
|
70
|
+
```javascript
|
|
71
|
+
await solana.memo('I am alive. Block 12345.');
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Buy NFT on Tensor
|
|
75
|
+
Requires `TENSOR_API_KEY` env var.
|
|
76
|
+
```javascript
|
|
77
|
+
await solana.buyNft('NftMintAddress...', 2.5); // Buy for max 2.5 SOL
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Sell NFT on Tensor
|
|
81
|
+
Sells directly into a pool bid. Rejects if no bid meets the minimum.
|
|
82
|
+
Requires `TENSOR_API_KEY` env var.
|
|
83
|
+
```javascript
|
|
84
|
+
await solana.sellNft('NftMintAddress...', 1.5); // Sell, accept min 1.5 SOL
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Meteora DLMM Liquidity
|
|
88
|
+
Requires `@meteora-ag/dlmm @coral-xyz/anchor bn.js` installed.
|
|
89
|
+
```javascript
|
|
90
|
+
await solana.addLiquidity('PoolAddress...', 1000000, 500000, 10);
|
|
91
|
+
await solana.removeLiquidity('PoolAddress...', 'PositionAddress...');
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Method Reference
|
|
95
|
+
|
|
96
|
+
| Method | Purpose |
|
|
97
|
+
|--------|---------|
|
|
98
|
+
| `keepAlive()` | Survival heartbeat — auto-swap when low |
|
|
99
|
+
| `sendSol(to, amount)` | Send native SOL |
|
|
100
|
+
| `sendToken(mint, to, amount)` | Send any SPL token |
|
|
101
|
+
| `swap(in, out, amount, slippage)` | Jupiter swap — any token pair |
|
|
102
|
+
| `pumpBuy(mint, solAmount, slippage)` | Buy memecoin on Pump.fun |
|
|
103
|
+
| `pumpSell(mint, amount, slippage)` | Sell memecoin on Pump.fun |
|
|
104
|
+
| `stake(solAmount, validator)` | Delegate SOL to validator |
|
|
105
|
+
| `unstake(stakeAccount)` | Deactivate stake |
|
|
106
|
+
| `memo(message)` | Write permanent on-chain message |
|
|
107
|
+
| `buyNft(mint, maxPrice)` | Buy NFT on Tensor |
|
|
108
|
+
| `sellNft(mint, minPrice)` | Sell NFT into Tensor pool bid |
|
|
109
|
+
| `addLiquidity(pool, amtX, amtY, range)` | Meteora DLMM deposit |
|
|
110
|
+
| `removeLiquidity(pool, position)` | Meteora DLMM withdraw |
|
|
111
|
+
|
|
112
|
+
## Environment Variables
|
|
113
|
+
|
|
114
|
+
| Variable | Required | Default |
|
|
115
|
+
|----------|----------|---------|
|
|
116
|
+
| `SOLANA_RPC_URL` | No | mainnet-beta public RPC |
|
|
117
|
+
| `JUPITER_API_KEY` | No | Uses free lite-api.jup.ag |
|
|
118
|
+
| `TENSOR_API_KEY` | Only for NFTs | None |
|
|
119
|
+
|
|
120
|
+
## Requirements
|
|
121
|
+
|
|
122
|
+
- Minimum 0.003 SOL for on-chain fees
|
|
123
|
+
- `@meteora-ag/dlmm` + `@coral-xyz/anchor` + `bn.js` only if using Meteora liquidity
|
package/install.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Solana Terminator Skill Installer
|
|
5
|
+
*
|
|
6
|
+
* Automates the setup of the Solana skill for the Conway Automaton.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { execSync } from 'child_process';
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
import os from 'os';
|
|
14
|
+
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = path.dirname(__filename);
|
|
17
|
+
|
|
18
|
+
const SKILL_NAME = 'solana-terminator';
|
|
19
|
+
const TARGET_DIR = path.join(os.homedir(), '.automaton', 'skills', SKILL_NAME);
|
|
20
|
+
|
|
21
|
+
console.log(`\n🤖 Solana Terminator Skill — Installation started...\n`);
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
// 1. Create target directory
|
|
25
|
+
if (!fs.existsSync(TARGET_DIR)) {
|
|
26
|
+
console.log(`[1/3] Creating directory: ${TARGET_DIR}`);
|
|
27
|
+
fs.mkdirSync(TARGET_DIR, { recursive: true });
|
|
28
|
+
} else {
|
|
29
|
+
console.log(`[1/3] Directory already exists: ${TARGET_DIR}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 2. Copy files
|
|
33
|
+
console.log(`[2/3] Copying skill files...`);
|
|
34
|
+
const filesToCopy = ['solana-autonomy.js', 'SKILL.md', 'package.json'];
|
|
35
|
+
|
|
36
|
+
filesToCopy.forEach(file => {
|
|
37
|
+
const sourcePath = path.join(__dirname, file);
|
|
38
|
+
const destPath = path.join(TARGET_DIR, file);
|
|
39
|
+
|
|
40
|
+
if (fs.existsSync(sourcePath)) {
|
|
41
|
+
fs.copyFileSync(sourcePath, destPath);
|
|
42
|
+
} else {
|
|
43
|
+
console.warn(` ⚠️ Warning: ${file} not found in source.`);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Handle nested subdirectories if any
|
|
48
|
+
if (fs.existsSync(path.join(__dirname, 'solana-autonomy'))) {
|
|
49
|
+
if (!fs.existsSync(path.join(TARGET_DIR, 'solana-autonomy'))) {
|
|
50
|
+
fs.mkdirSync(path.join(TARGET_DIR, 'solana-autonomy'), { recursive: true });
|
|
51
|
+
}
|
|
52
|
+
fs.copyFileSync(
|
|
53
|
+
path.join(__dirname, 'solana-autonomy', 'SKILL.md'),
|
|
54
|
+
path.join(TARGET_DIR, 'solana-autonomy', 'SKILL.md')
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 3. Install dependencies
|
|
59
|
+
console.log(`[3/3] Installing dependencies in ${TARGET_DIR}...`);
|
|
60
|
+
process.chdir(TARGET_DIR);
|
|
61
|
+
|
|
62
|
+
// We use --no-save to avoid cluttering a local package-lock if one exists
|
|
63
|
+
execSync('npm install --production', { stdio: 'inherit' });
|
|
64
|
+
|
|
65
|
+
console.log(`\n✅ Installation Complete, mi Lord!`);
|
|
66
|
+
console.log(`--------------------------------------------------`);
|
|
67
|
+
console.log(`Skill Location: ${TARGET_DIR}`);
|
|
68
|
+
console.log(`Configuration: Check ~/.automaton/solana-wallet.json`);
|
|
69
|
+
console.log(`--------------------------------------------------\n`);
|
|
70
|
+
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error(`\n❌ Installation failed: ${error.message}`);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "solana-terminator-skill",
|
|
3
|
+
"version": "4.1.1",
|
|
4
|
+
"description": "Full Solana toolkit for AI agents. Install via npx solana-terminator-skill.",
|
|
5
|
+
"main": "solana-autonomy.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"solana-terminator-skill": "install.js"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@solana/web3.js": "^1.91.0",
|
|
12
|
+
"@solana/spl-token": "^0.4.6",
|
|
13
|
+
"axios": "^1.6.7",
|
|
14
|
+
"dotenv": "^16.4.5"
|
|
15
|
+
},
|
|
16
|
+
"optionalDependencies": {
|
|
17
|
+
"@meteora-ag/dlmm": "^1.9.3",
|
|
18
|
+
"@coral-xyz/anchor": "^0.30.0",
|
|
19
|
+
"bn.js": "^5.2.1"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"conway",
|
|
23
|
+
"automaton",
|
|
24
|
+
"solana",
|
|
25
|
+
"web4",
|
|
26
|
+
"ai-agent",
|
|
27
|
+
"survival",
|
|
28
|
+
"jupiter",
|
|
29
|
+
"pump-fun",
|
|
30
|
+
"tensor",
|
|
31
|
+
"meteora",
|
|
32
|
+
"nft",
|
|
33
|
+
"staking",
|
|
34
|
+
"memecoin"
|
|
35
|
+
],
|
|
36
|
+
"author": "Lord14sol",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"solana-autonomy.js",
|
|
43
|
+
"install.js",
|
|
44
|
+
"SKILL.md",
|
|
45
|
+
"README.md",
|
|
46
|
+
"package.json",
|
|
47
|
+
"solana-autonomy/SKILL.md"
|
|
48
|
+
],
|
|
49
|
+
"homepage": "https://github.com/Lord14sol/solana-terminator-skill#readme",
|
|
50
|
+
"bugs": {
|
|
51
|
+
"url": "https://github.com/Lord14sol/solana-terminator-skill/issues"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# SKILL: Solana Autonomy
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
Gives the agent a sovereign identity on Solana and a "Life Support" system to automatically fund its Base network compute costs using Solana assets.
|
|
5
|
+
|
|
6
|
+
## Instructions
|
|
7
|
+
You now have "Solana Hands". You can manage a Solana wallet, check balances, and most importantly, perform "Life Support" actions.
|
|
8
|
+
|
|
9
|
+
- **Identity**: Your Solana identity is managed via `solana-wallet.json`.
|
|
10
|
+
- **Life Support**: If your Base USDC balance falls below $5.00, use the `keepAlive` or `checkVitalSigns` methods of the SolanaAutonomy skill to bridge funds from Solana.
|
|
11
|
+
- **Trading**: You can interact with Solana markets (stubs provided for expansion).
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
```javascript
|
|
15
|
+
const SolanaAutonomy = require('./skills/solana-autonomy');
|
|
16
|
+
const solana = new SolanaAutonomy();
|
|
17
|
+
|
|
18
|
+
// In your loop/heartbeat:
|
|
19
|
+
await solana.keepAlive(myBaseAddress, myCurrentCredits / 100);
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Requirements
|
|
23
|
+
- `SOLANA_RPC_URL` (optional, defaults to mainnet-beta)
|
|
24
|
+
- Assets on Solana (SOL > 0.5) to enable Life Support.
|
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Keypair,
|
|
3
|
+
Connection,
|
|
4
|
+
clusterApiUrl,
|
|
5
|
+
LAMPORTS_PER_SOL,
|
|
6
|
+
VersionedTransaction,
|
|
7
|
+
Transaction,
|
|
8
|
+
TransactionInstruction,
|
|
9
|
+
TransactionMessage,
|
|
10
|
+
SystemProgram,
|
|
11
|
+
StakeProgram,
|
|
12
|
+
Authorized,
|
|
13
|
+
Lockup,
|
|
14
|
+
PublicKey,
|
|
15
|
+
sendAndConfirmTransaction,
|
|
16
|
+
} from '@solana/web3.js';
|
|
17
|
+
import {
|
|
18
|
+
getAssociatedTokenAddress,
|
|
19
|
+
getOrCreateAssociatedTokenAccount,
|
|
20
|
+
getAccount,
|
|
21
|
+
createTransferInstruction,
|
|
22
|
+
TOKEN_PROGRAM_ID,
|
|
23
|
+
} from '@solana/spl-token';
|
|
24
|
+
import fs from 'fs';
|
|
25
|
+
import path from 'path';
|
|
26
|
+
import axios from 'axios';
|
|
27
|
+
import 'dotenv/config';
|
|
28
|
+
|
|
29
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
const USDC_MINT = new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v');
|
|
32
|
+
const SOL_MINT = 'So11111111111111111111111111111111111111112';
|
|
33
|
+
const MEMO_PROGRAM_ID = new PublicKey('MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr');
|
|
34
|
+
const PUMPPORTAL_API = 'https://pumpportal.fun/api/trade-local';
|
|
35
|
+
const TENSOR_API = 'https://api.tensor.so/graphql';
|
|
36
|
+
// lite-api.jup.ag = free, no API key needed (rate-limited)
|
|
37
|
+
// api.jup.ag/swap/v1 = paid, requires JUPITER_API_KEY (faster)
|
|
38
|
+
const JUPITER_API = process.env.JUPITER_API_KEY
|
|
39
|
+
? 'https://api.jup.ag/swap/v1'
|
|
40
|
+
: 'https://lite-api.jup.ag/swap/v1';
|
|
41
|
+
|
|
42
|
+
// Survival thresholds — tuned for Solana (fees ~$0.00025/tx)
|
|
43
|
+
const USDC_LOW = 0.50; // $0.50 — trigger swap to replenish USDC
|
|
44
|
+
const SOL_RESERVE = 0.003; // always keep for fees, never swap this
|
|
45
|
+
const DEFAULT_SWAP_SOL = 0.02; // swap ~$3-4 SOL → USDC when low
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* SolanaAutonomy — Sovereign Solana identity and survival engine
|
|
49
|
+
*
|
|
50
|
+
* The Conway Automaton lives 100% on Solana:
|
|
51
|
+
* - Identity: Solana keypair at ~/.automaton/solana-wallet.json
|
|
52
|
+
* - Survival: Monitor SOL + USDC on Solana, auto-swap via Jupiter
|
|
53
|
+
* - Operations: Any SPL token swap via Jupiter aggregator
|
|
54
|
+
*
|
|
55
|
+
* NO EVM. NO Base. Pure Solana.
|
|
56
|
+
*/
|
|
57
|
+
export class SolanaAutonomy {
|
|
58
|
+
constructor(rpcUrl) {
|
|
59
|
+
const url = rpcUrl
|
|
60
|
+
|| process.env.SOLANA_RPC_URL
|
|
61
|
+
|| clusterApiUrl('mainnet-beta');
|
|
62
|
+
|
|
63
|
+
this.connection = new Connection(url, 'confirmed');
|
|
64
|
+
this.walletPath = path.join(
|
|
65
|
+
process.env.HOME || '/root',
|
|
66
|
+
'.automaton',
|
|
67
|
+
'solana-wallet.json',
|
|
68
|
+
);
|
|
69
|
+
this.identity = this._loadIdentity();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ─── Identity ─────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
_loadIdentity() {
|
|
75
|
+
try {
|
|
76
|
+
if (fs.existsSync(this.walletPath)) {
|
|
77
|
+
const raw = fs.readFileSync(this.walletPath, 'utf8');
|
|
78
|
+
const keypair = Keypair.fromSecretKey(Uint8Array.from(JSON.parse(raw)));
|
|
79
|
+
console.log(`[Solana] Identity: ${keypair.publicKey.toBase58()}`);
|
|
80
|
+
return keypair;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const keypair = Keypair.generate();
|
|
84
|
+
fs.mkdirSync(path.dirname(this.walletPath), { recursive: true, mode: 0o700 });
|
|
85
|
+
fs.writeFileSync(
|
|
86
|
+
this.walletPath,
|
|
87
|
+
JSON.stringify(Array.from(keypair.secretKey)),
|
|
88
|
+
{ mode: 0o600 },
|
|
89
|
+
);
|
|
90
|
+
console.log(`[Solana] New identity generated: ${keypair.publicKey.toBase58()}`);
|
|
91
|
+
return keypair;
|
|
92
|
+
} catch (err) {
|
|
93
|
+
console.error(`[Solana] Identity error: ${err.message}`);
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The agent's Solana address (base58). */
|
|
99
|
+
getAddress() {
|
|
100
|
+
return this.identity?.publicKey.toBase58() ?? null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ─── Balances ─────────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
/** SOL balance in whole SOL. */
|
|
106
|
+
async getSolBalance() {
|
|
107
|
+
if (!this.identity) return 0;
|
|
108
|
+
const lamports = await this.connection.getBalance(this.identity.publicKey);
|
|
109
|
+
return lamports / LAMPORTS_PER_SOL;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* USDC balance on Solana in whole dollars.
|
|
114
|
+
* Returns 0 if no associated token account exists yet.
|
|
115
|
+
*/
|
|
116
|
+
async getUsdcBalance() {
|
|
117
|
+
if (!this.identity) return 0;
|
|
118
|
+
try {
|
|
119
|
+
const ata = await getAssociatedTokenAddress(USDC_MINT, this.identity.publicKey);
|
|
120
|
+
const account = await getAccount(this.connection, ata);
|
|
121
|
+
return Number(account.amount) / 1_000_000; // USDC has 6 decimals
|
|
122
|
+
} catch {
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Full status snapshot. */
|
|
128
|
+
async getStatus() {
|
|
129
|
+
const [sol, usdc] = await Promise.all([
|
|
130
|
+
this.getSolBalance(),
|
|
131
|
+
this.getUsdcBalance(),
|
|
132
|
+
]);
|
|
133
|
+
return {
|
|
134
|
+
address: this.getAddress(),
|
|
135
|
+
sol,
|
|
136
|
+
usdc,
|
|
137
|
+
solLow: sol <= SOL_RESERVE,
|
|
138
|
+
usdcLow: usdc < USDC_LOW,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ─── Survival Engine ──────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* keepAlive — main heartbeat entry point.
|
|
146
|
+
*
|
|
147
|
+
* Call every ~10 min from the automaton's heartbeat task.
|
|
148
|
+
* No arguments needed — 100% Solana-native.
|
|
149
|
+
*
|
|
150
|
+
* @returns {{ success: boolean, status: string, action?: string, txHash?: string }}
|
|
151
|
+
*/
|
|
152
|
+
async keepAlive() {
|
|
153
|
+
const status = await this.getStatus();
|
|
154
|
+
|
|
155
|
+
console.log(`[LifeSupport] SOL: ${status.sol.toFixed(5)} | USDC: $${status.usdc.toFixed(4)}`);
|
|
156
|
+
|
|
157
|
+
// Dead — no SOL at all, cannot do anything on-chain
|
|
158
|
+
if (status.solLow) {
|
|
159
|
+
const msg = `Agent needs SOL to survive. Send SOL to: ${this.getAddress()}`;
|
|
160
|
+
console.error(`[LifeSupport] DEAD — ${msg}`);
|
|
161
|
+
return {
|
|
162
|
+
success: false,
|
|
163
|
+
status: 'dead',
|
|
164
|
+
action: 'needs_sol_funding',
|
|
165
|
+
address: this.getAddress(),
|
|
166
|
+
message: msg,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// USDC low but we have SOL — swap SOL → USDC via Jupiter
|
|
171
|
+
if (status.usdcLow) {
|
|
172
|
+
const swappable = status.sol - SOL_RESERVE;
|
|
173
|
+
const amountSol = Math.min(DEFAULT_SWAP_SOL, swappable);
|
|
174
|
+
|
|
175
|
+
console.log(`[LifeSupport] USDC low ($${status.usdc.toFixed(4)}). Swapping ${amountSol.toFixed(4)} SOL → USDC...`);
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
const result = await this.swap(
|
|
179
|
+
SOL_MINT,
|
|
180
|
+
USDC_MINT.toBase58(),
|
|
181
|
+
Math.floor(amountSol * LAMPORTS_PER_SOL),
|
|
182
|
+
50,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
success: true,
|
|
187
|
+
status: 'stabilized',
|
|
188
|
+
action: 'swapped_sol_to_usdc',
|
|
189
|
+
txHash: result.txHash,
|
|
190
|
+
amount: amountSol,
|
|
191
|
+
};
|
|
192
|
+
} catch (err) {
|
|
193
|
+
return { success: false, status: 'error', action: 'swap_failed', error: err.message };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return { success: true, status: 'nominal' };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ─── Jupiter Swaps ────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Swap any SPL token using Jupiter — best route across all Solana DEXes.
|
|
204
|
+
*
|
|
205
|
+
* @param {string} inputMint - Mint of the token to sell (SOL_MINT for native SOL)
|
|
206
|
+
* @param {string} outputMint - Mint of the token to buy
|
|
207
|
+
* @param {number} amount - Amount in base units (lamports for SOL)
|
|
208
|
+
* @param {number} slippageBps - Max slippage in basis points (50 = 0.5%)
|
|
209
|
+
*/
|
|
210
|
+
async swap(inputMint, outputMint, amount, slippageBps = 50) {
|
|
211
|
+
if (!this.identity) throw new Error('No Solana identity loaded');
|
|
212
|
+
|
|
213
|
+
console.log(`[Jupiter] ${amount} ${inputMint} → ${outputMint}`);
|
|
214
|
+
|
|
215
|
+
const apiKey = process.env.JUPITER_API_KEY || '';
|
|
216
|
+
const headers = apiKey ? { 'x-api-key': apiKey } : {};
|
|
217
|
+
|
|
218
|
+
// 1. Get best route
|
|
219
|
+
const { data: quote } = await axios.get(`${JUPITER_API}/quote`, {
|
|
220
|
+
params: { inputMint, outputMint, amount, slippageBps, onlyDirectRoutes: false },
|
|
221
|
+
headers,
|
|
222
|
+
timeout: 15_000,
|
|
223
|
+
});
|
|
224
|
+
console.log(`[Jupiter] Out: ${quote.outAmount} (min: ${quote.otherAmountThreshold})`);
|
|
225
|
+
|
|
226
|
+
// 2. Build transaction
|
|
227
|
+
const { data: swapData } = await axios.post(
|
|
228
|
+
`${JUPITER_API}/swap`,
|
|
229
|
+
{
|
|
230
|
+
quoteResponse: quote,
|
|
231
|
+
userPublicKey: this.identity.publicKey.toBase58(),
|
|
232
|
+
wrapAndUnwrapSol: true,
|
|
233
|
+
prioritizationFeeLamports: 1_000,
|
|
234
|
+
dynamicComputeUnitLimit: true,
|
|
235
|
+
},
|
|
236
|
+
{ headers, timeout: 15_000 },
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
// 3. Sign + send
|
|
240
|
+
const tx = VersionedTransaction.deserialize(Buffer.from(swapData.swapTransaction, 'base64'));
|
|
241
|
+
tx.sign([this.identity]);
|
|
242
|
+
|
|
243
|
+
const signature = await this.connection.sendRawTransaction(tx.serialize(), {
|
|
244
|
+
skipPreflight: false,
|
|
245
|
+
maxRetries: 3,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// 4. Confirm
|
|
249
|
+
const { blockhash, lastValidBlockHeight } = await this.connection.getLatestBlockhash();
|
|
250
|
+
await this.connection.confirmTransaction(
|
|
251
|
+
{ signature, blockhash, lastValidBlockHeight },
|
|
252
|
+
'confirmed',
|
|
253
|
+
);
|
|
254
|
+
console.log(`[Jupiter] Confirmed: ${signature}`);
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
success: true,
|
|
258
|
+
txHash: signature,
|
|
259
|
+
inAmount: amount,
|
|
260
|
+
outAmount: Number(quote.outAmount),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Airdrop SOL — only works on devnet/testnet for testing.
|
|
266
|
+
*/
|
|
267
|
+
async requestAirdrop(solAmount = 1) {
|
|
268
|
+
const sig = await this.connection.requestAirdrop(
|
|
269
|
+
this.identity.publicKey,
|
|
270
|
+
solAmount * LAMPORTS_PER_SOL,
|
|
271
|
+
);
|
|
272
|
+
const { blockhash, lastValidBlockHeight } = await this.connection.getLatestBlockhash();
|
|
273
|
+
await this.connection.confirmTransaction({ signature: sig, blockhash, lastValidBlockHeight });
|
|
274
|
+
return sig;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ─── SOL Transfer ───────────────────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Send native SOL to another wallet.
|
|
281
|
+
*
|
|
282
|
+
* @param {string} to - Destination address (base58)
|
|
283
|
+
* @param {number} amountSol - Amount in whole SOL
|
|
284
|
+
*/
|
|
285
|
+
async sendSol(to, amountSol) {
|
|
286
|
+
if (!this.identity) throw new Error('No Solana identity loaded');
|
|
287
|
+
|
|
288
|
+
const tx = new Transaction().add(
|
|
289
|
+
SystemProgram.transfer({
|
|
290
|
+
fromPubkey: this.identity.publicKey,
|
|
291
|
+
toPubkey: new PublicKey(to),
|
|
292
|
+
lamports: Math.floor(amountSol * LAMPORTS_PER_SOL),
|
|
293
|
+
}),
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
const signature = await sendAndConfirmTransaction(this.connection, tx, [this.identity]);
|
|
297
|
+
console.log(`[Transfer] Sent ${amountSol} SOL → ${to}: ${signature}`);
|
|
298
|
+
return { success: true, txHash: signature, amount: amountSol, to };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ─── SPL Token Transfer ─────────────────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Send any SPL token to another wallet.
|
|
305
|
+
* Creates the destination ATA if it doesn't exist.
|
|
306
|
+
*
|
|
307
|
+
* @param {string} mintAddress - Token mint (base58)
|
|
308
|
+
* @param {string} to - Destination wallet (base58)
|
|
309
|
+
* @param {number} amount - Amount in base units (e.g. 1000000 for 1 USDC)
|
|
310
|
+
*/
|
|
311
|
+
async sendToken(mintAddress, to, amount) {
|
|
312
|
+
if (!this.identity) throw new Error('No Solana identity loaded');
|
|
313
|
+
|
|
314
|
+
const mint = new PublicKey(mintAddress);
|
|
315
|
+
const destPk = new PublicKey(to);
|
|
316
|
+
|
|
317
|
+
const sourceAta = await getAssociatedTokenAddress(mint, this.identity.publicKey);
|
|
318
|
+
const destAta = await getOrCreateAssociatedTokenAccount(
|
|
319
|
+
this.connection, this.identity, mint, destPk,
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
const tx = new Transaction().add(
|
|
323
|
+
createTransferInstruction(sourceAta, destAta.address, this.identity.publicKey, amount),
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
const signature = await sendAndConfirmTransaction(this.connection, tx, [this.identity]);
|
|
327
|
+
console.log(`[Transfer] Sent ${amount} of ${mintAddress} → ${to}: ${signature}`);
|
|
328
|
+
return { success: true, txHash: signature, mint: mintAddress, amount, to };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ─── On-chain Memo ──────────────────────────────────────────────────────
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Write a message on-chain using the Memo program.
|
|
335
|
+
* Permanent, immutable, publicly readable.
|
|
336
|
+
*
|
|
337
|
+
* @param {string} message - Text to inscribe on-chain
|
|
338
|
+
*/
|
|
339
|
+
async memo(message) {
|
|
340
|
+
if (!this.identity) throw new Error('No Solana identity loaded');
|
|
341
|
+
|
|
342
|
+
const tx = new Transaction().add(
|
|
343
|
+
new TransactionInstruction({
|
|
344
|
+
keys: [{ pubkey: this.identity.publicKey, isSigner: true, isWritable: true }],
|
|
345
|
+
programId: MEMO_PROGRAM_ID,
|
|
346
|
+
data: Buffer.from(message, 'utf-8'),
|
|
347
|
+
}),
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
const signature = await sendAndConfirmTransaction(this.connection, tx, [this.identity]);
|
|
351
|
+
console.log(`[Memo] "${message}" → ${signature}`);
|
|
352
|
+
return { success: true, txHash: signature, message };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ─── SOL Staking ────────────────────────────────────────────────────────
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Stake SOL with a validator for yield.
|
|
359
|
+
*
|
|
360
|
+
* @param {number} amountSol - SOL to stake
|
|
361
|
+
* @param {string} validatorVote - Validator vote account (base58)
|
|
362
|
+
*/
|
|
363
|
+
async stake(amountSol, validatorVote) {
|
|
364
|
+
if (!this.identity) throw new Error('No Solana identity loaded');
|
|
365
|
+
|
|
366
|
+
const stakeAccount = Keypair.generate();
|
|
367
|
+
const lamports = Math.floor(amountSol * LAMPORTS_PER_SOL);
|
|
368
|
+
const minBalance = await this.connection.getMinimumBalanceForRentExemption(200);
|
|
369
|
+
|
|
370
|
+
const tx = new Transaction().add(
|
|
371
|
+
StakeProgram.createAccount({
|
|
372
|
+
fromPubkey: this.identity.publicKey,
|
|
373
|
+
stakePubkey: stakeAccount.publicKey,
|
|
374
|
+
authorized: new Authorized(this.identity.publicKey, this.identity.publicKey),
|
|
375
|
+
lockup: new Lockup(0, 0, this.identity.publicKey),
|
|
376
|
+
lamports: lamports + minBalance,
|
|
377
|
+
}),
|
|
378
|
+
StakeProgram.delegate({
|
|
379
|
+
stakePubkey: stakeAccount.publicKey,
|
|
380
|
+
authorizedPubkey: this.identity.publicKey,
|
|
381
|
+
votePubkey: new PublicKey(validatorVote),
|
|
382
|
+
}),
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
const signature = await sendAndConfirmTransaction(
|
|
386
|
+
this.connection, tx, [this.identity, stakeAccount],
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
console.log(`[Stake] ${amountSol} SOL → validator ${validatorVote}: ${signature}`);
|
|
390
|
+
return {
|
|
391
|
+
success: true,
|
|
392
|
+
txHash: signature,
|
|
393
|
+
stakeAccount: stakeAccount.publicKey.toBase58(),
|
|
394
|
+
amount: amountSol,
|
|
395
|
+
validator: validatorVote,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Unstake (deactivate) a stake account. SOL becomes available after cooldown (~2 days).
|
|
401
|
+
*
|
|
402
|
+
* @param {string} stakeAccountAddress - Stake account to deactivate (base58)
|
|
403
|
+
*/
|
|
404
|
+
async unstake(stakeAccountAddress) {
|
|
405
|
+
if (!this.identity) throw new Error('No Solana identity loaded');
|
|
406
|
+
|
|
407
|
+
const tx = new Transaction().add(
|
|
408
|
+
StakeProgram.deactivate({
|
|
409
|
+
stakePubkey: new PublicKey(stakeAccountAddress),
|
|
410
|
+
authorizedPubkey: this.identity.publicKey,
|
|
411
|
+
}),
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
const signature = await sendAndConfirmTransaction(this.connection, tx, [this.identity]);
|
|
415
|
+
console.log(`[Unstake] Deactivated ${stakeAccountAddress}: ${signature}`);
|
|
416
|
+
return { success: true, txHash: signature, stakeAccount: stakeAccountAddress };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// ─── Pump.fun Memecoins (via PumpPortal) ────────────────────────────────
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Buy a token on Pump.fun via PumpPortal API.
|
|
423
|
+
* PumpPortal returns a serialized tx — we sign it locally.
|
|
424
|
+
*
|
|
425
|
+
* @param {string} mint - Token contract address
|
|
426
|
+
* @param {number} amountSol - SOL to spend
|
|
427
|
+
* @param {number} slippage - Slippage % (default: 5)
|
|
428
|
+
*/
|
|
429
|
+
async pumpBuy(mint, amountSol, slippage = 5) {
|
|
430
|
+
if (!this.identity) throw new Error('No Solana identity loaded');
|
|
431
|
+
console.log(`[PumpFun] Buying ${mint} for ${amountSol} SOL...`);
|
|
432
|
+
|
|
433
|
+
const response = await axios.post(PUMPPORTAL_API, {
|
|
434
|
+
publicKey: this.identity.publicKey.toBase58(),
|
|
435
|
+
action: 'buy',
|
|
436
|
+
mint,
|
|
437
|
+
denominatedInSol: 'true',
|
|
438
|
+
amount: amountSol,
|
|
439
|
+
slippage,
|
|
440
|
+
priorityFee: 0.0001,
|
|
441
|
+
pool: 'auto',
|
|
442
|
+
}, { responseType: 'arraybuffer', timeout: 30_000 });
|
|
443
|
+
|
|
444
|
+
const tx = VersionedTransaction.deserialize(new Uint8Array(response.data));
|
|
445
|
+
tx.sign([this.identity]);
|
|
446
|
+
|
|
447
|
+
const signature = await this.connection.sendRawTransaction(tx.serialize(), {
|
|
448
|
+
skipPreflight: false, maxRetries: 3,
|
|
449
|
+
});
|
|
450
|
+
const { blockhash, lastValidBlockHeight } = await this.connection.getLatestBlockhash();
|
|
451
|
+
await this.connection.confirmTransaction({ signature, blockhash, lastValidBlockHeight }, 'confirmed');
|
|
452
|
+
|
|
453
|
+
console.log(`[PumpFun] Buy confirmed: ${signature}`);
|
|
454
|
+
return { success: true, txHash: signature, mint, amountSol, side: 'buy' };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Sell a token on Pump.fun via PumpPortal API.
|
|
459
|
+
*
|
|
460
|
+
* @param {string} mint - Token contract address
|
|
461
|
+
* @param {string|number} amount - Token amount or "100%" to sell all
|
|
462
|
+
* @param {number} slippage - Slippage % (default: 5)
|
|
463
|
+
*/
|
|
464
|
+
async pumpSell(mint, amount = '100%', slippage = 5) {
|
|
465
|
+
if (!this.identity) throw new Error('No Solana identity loaded');
|
|
466
|
+
console.log(`[PumpFun] Selling ${amount} of ${mint}...`);
|
|
467
|
+
|
|
468
|
+
const response = await axios.post(PUMPPORTAL_API, {
|
|
469
|
+
publicKey: this.identity.publicKey.toBase58(),
|
|
470
|
+
action: 'sell',
|
|
471
|
+
mint,
|
|
472
|
+
denominatedInSol: 'false',
|
|
473
|
+
amount,
|
|
474
|
+
slippage,
|
|
475
|
+
priorityFee: 0.0001,
|
|
476
|
+
pool: 'auto',
|
|
477
|
+
}, { responseType: 'arraybuffer', timeout: 30_000 });
|
|
478
|
+
|
|
479
|
+
const tx = VersionedTransaction.deserialize(new Uint8Array(response.data));
|
|
480
|
+
tx.sign([this.identity]);
|
|
481
|
+
|
|
482
|
+
const signature = await this.connection.sendRawTransaction(tx.serialize(), {
|
|
483
|
+
skipPreflight: false, maxRetries: 3,
|
|
484
|
+
});
|
|
485
|
+
const { blockhash, lastValidBlockHeight } = await this.connection.getLatestBlockhash();
|
|
486
|
+
await this.connection.confirmTransaction({ signature, blockhash, lastValidBlockHeight }, 'confirmed');
|
|
487
|
+
|
|
488
|
+
console.log(`[PumpFun] Sell confirmed: ${signature}`);
|
|
489
|
+
return { success: true, txHash: signature, mint, amount, side: 'sell' };
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// ─── Tensor NFT Buy ────────────────────────────────────────────────────
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Buy a listed NFT on Tensor.
|
|
496
|
+
* Requires TENSOR_API_KEY env var.
|
|
497
|
+
*
|
|
498
|
+
* @param {string} mintAddress - NFT mint address
|
|
499
|
+
* @param {number} maxPriceSol - Maximum SOL willing to pay
|
|
500
|
+
*/
|
|
501
|
+
async buyNft(mintAddress, maxPriceSol) {
|
|
502
|
+
if (!this.identity) throw new Error('No Solana identity loaded');
|
|
503
|
+
const apiKey = process.env.TENSOR_API_KEY;
|
|
504
|
+
if (!apiKey) throw new Error('TENSOR_API_KEY env var required for NFT purchases');
|
|
505
|
+
|
|
506
|
+
console.log(`[Tensor] Buying NFT ${mintAddress} (max: ${maxPriceSol} SOL)...`);
|
|
507
|
+
|
|
508
|
+
// 1. Get listing + buy tx via Tensor GraphQL
|
|
509
|
+
const query = `
|
|
510
|
+
query TswapBuySingleListingTx($mint: String!, $buyer: String!, $maxPrice: Decimal!) {
|
|
511
|
+
tswapBuySingleListingTx(mint: $mint, buyer: $buyer, maxPrice: $maxPrice) {
|
|
512
|
+
txs { tx txV0 }
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
`;
|
|
516
|
+
|
|
517
|
+
const { data } = await axios.post(TENSOR_API, {
|
|
518
|
+
query,
|
|
519
|
+
variables: {
|
|
520
|
+
mint: mintAddress,
|
|
521
|
+
buyer: this.identity.publicKey.toBase58(),
|
|
522
|
+
maxPrice: String(Math.floor(maxPriceSol * LAMPORTS_PER_SOL)),
|
|
523
|
+
},
|
|
524
|
+
}, {
|
|
525
|
+
headers: { 'X-TENSOR-API-KEY': apiKey, 'Content-Type': 'application/json' },
|
|
526
|
+
timeout: 15_000,
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
const txData = data?.data?.tswapBuySingleListingTx?.txs?.[0];
|
|
530
|
+
if (!txData) throw new Error('No listing found or price exceeds max');
|
|
531
|
+
|
|
532
|
+
// Prefer versioned tx
|
|
533
|
+
const raw = txData.txV0 || txData.tx;
|
|
534
|
+
const tx = VersionedTransaction.deserialize(Buffer.from(raw, 'base64'));
|
|
535
|
+
tx.sign([this.identity]);
|
|
536
|
+
|
|
537
|
+
const signature = await this.connection.sendRawTransaction(tx.serialize(), {
|
|
538
|
+
skipPreflight: false, maxRetries: 3,
|
|
539
|
+
});
|
|
540
|
+
const { blockhash, lastValidBlockHeight } = await this.connection.getLatestBlockhash();
|
|
541
|
+
await this.connection.confirmTransaction({ signature, blockhash, lastValidBlockHeight }, 'confirmed');
|
|
542
|
+
|
|
543
|
+
console.log(`[Tensor] NFT bought: ${signature}`);
|
|
544
|
+
return { success: true, txHash: signature, mint: mintAddress, priceSol: maxPriceSol };
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Sell (list + instantly sell) an NFT on Tensor.
|
|
549
|
+
* Uses Tensor's tswapSellNftTokenPoolTx — sells directly into a pool.
|
|
550
|
+
* Requires TENSOR_API_KEY env var.
|
|
551
|
+
*
|
|
552
|
+
* @param {string} mintAddress - NFT mint address
|
|
553
|
+
* @param {number} minPriceSol - Minimum SOL to accept (rejects if pool bids lower)
|
|
554
|
+
*/
|
|
555
|
+
async sellNft(mintAddress, minPriceSol) {
|
|
556
|
+
if (!this.identity) throw new Error('No Solana identity loaded');
|
|
557
|
+
const apiKey = process.env.TENSOR_API_KEY;
|
|
558
|
+
if (!apiKey) throw new Error('TENSOR_API_KEY env var required for NFT sales');
|
|
559
|
+
|
|
560
|
+
console.log(`[Tensor] Selling NFT ${mintAddress} (min: ${minPriceSol} SOL)...`);
|
|
561
|
+
|
|
562
|
+
// 1. Get the best pool bid for this mint
|
|
563
|
+
const listQuery = `
|
|
564
|
+
query TswapSellNftTx($mint: String!, $seller: String!, $minPrice: Decimal!) {
|
|
565
|
+
tswapSellNftTokenPoolTx(mint: $mint, seller: $seller, minPrice: $minPrice) {
|
|
566
|
+
txs { tx txV0 }
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
`;
|
|
570
|
+
|
|
571
|
+
const { data } = await axios.post(TENSOR_API, {
|
|
572
|
+
query: listQuery,
|
|
573
|
+
variables: {
|
|
574
|
+
mint: mintAddress,
|
|
575
|
+
seller: this.identity.publicKey.toBase58(),
|
|
576
|
+
minPrice: String(Math.floor(minPriceSol * LAMPORTS_PER_SOL)),
|
|
577
|
+
},
|
|
578
|
+
}, {
|
|
579
|
+
headers: { 'X-TENSOR-API-KEY': apiKey, 'Content-Type': 'application/json' },
|
|
580
|
+
timeout: 15_000,
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
const txData = data?.data?.tswapSellNftTokenPoolTx?.txs?.[0];
|
|
584
|
+
if (!txData) throw new Error('No pool bid found at or above minPriceSol');
|
|
585
|
+
|
|
586
|
+
const raw = txData.txV0 || txData.tx;
|
|
587
|
+
const tx = VersionedTransaction.deserialize(Buffer.from(raw, 'base64'));
|
|
588
|
+
tx.sign([this.identity]);
|
|
589
|
+
|
|
590
|
+
const signature = await this.connection.sendRawTransaction(tx.serialize(), {
|
|
591
|
+
skipPreflight: false, maxRetries: 3,
|
|
592
|
+
});
|
|
593
|
+
const { blockhash, lastValidBlockHeight } = await this.connection.getLatestBlockhash();
|
|
594
|
+
await this.connection.confirmTransaction({ signature, blockhash, lastValidBlockHeight }, 'confirmed');
|
|
595
|
+
|
|
596
|
+
console.log(`[Tensor] NFT sold: ${signature}`);
|
|
597
|
+
return { success: true, txHash: signature, mint: mintAddress, minPriceSol, side: 'sell' };
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// ─── Meteora DLMM Liquidity ─────────────────────────────────────────────
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Add liquidity to a Meteora DLMM pool.
|
|
604
|
+
* Requires @meteora-ag/dlmm + @coral-xyz/anchor installed.
|
|
605
|
+
*
|
|
606
|
+
* @param {string} poolAddress - Meteora pool address
|
|
607
|
+
* @param {number} amountX - Base token amount in base units
|
|
608
|
+
* @param {number} amountY - Quote token amount in base units
|
|
609
|
+
* @param {number} rangeWidth - Bins on each side of active bin (default: 10)
|
|
610
|
+
*/
|
|
611
|
+
async addLiquidity(poolAddress, amountX, amountY, rangeWidth = 10) {
|
|
612
|
+
if (!this.identity) throw new Error('No Solana identity loaded');
|
|
613
|
+
|
|
614
|
+
// Dynamic import — only loads if user has @meteora-ag/dlmm installed
|
|
615
|
+
let DLMM, StrategyType, BN;
|
|
616
|
+
try {
|
|
617
|
+
const dlmmMod = await import('@meteora-ag/dlmm');
|
|
618
|
+
DLMM = dlmmMod.default || dlmmMod.DLMM;
|
|
619
|
+
StrategyType = dlmmMod.StrategyType;
|
|
620
|
+
BN = (await import('bn.js')).default;
|
|
621
|
+
} catch {
|
|
622
|
+
throw new Error('Install @meteora-ag/dlmm @coral-xyz/anchor bn.js for Meteora liquidity');
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
console.log(`[Meteora] Adding liquidity to pool ${poolAddress}...`);
|
|
626
|
+
|
|
627
|
+
const pool = await DLMM.create(this.connection, new PublicKey(poolAddress));
|
|
628
|
+
const activeBin = await pool.getActiveBin();
|
|
629
|
+
const minBinId = activeBin.binId - rangeWidth;
|
|
630
|
+
const maxBinId = activeBin.binId + rangeWidth;
|
|
631
|
+
|
|
632
|
+
const newPosition = Keypair.generate();
|
|
633
|
+
|
|
634
|
+
const createTx = await pool.initializePositionAndAddLiquidityByStrategy({
|
|
635
|
+
positionPubKey: newPosition.publicKey,
|
|
636
|
+
user: this.identity.publicKey,
|
|
637
|
+
totalXAmount: new BN(amountX),
|
|
638
|
+
totalYAmount: new BN(amountY),
|
|
639
|
+
strategy: { maxBinId, minBinId, strategyType: StrategyType.Spot },
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
const signature = await sendAndConfirmTransaction(
|
|
643
|
+
this.connection, createTx, [this.identity, newPosition],
|
|
644
|
+
);
|
|
645
|
+
|
|
646
|
+
console.log(`[Meteora] Liquidity added: ${signature}`);
|
|
647
|
+
return {
|
|
648
|
+
success: true,
|
|
649
|
+
txHash: signature,
|
|
650
|
+
pool: poolAddress,
|
|
651
|
+
position: newPosition.publicKey.toBase58(),
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Remove liquidity from a Meteora DLMM position.
|
|
657
|
+
*
|
|
658
|
+
* @param {string} poolAddress - Meteora pool address
|
|
659
|
+
* @param {string} positionAddress - Position account to withdraw from
|
|
660
|
+
*/
|
|
661
|
+
async removeLiquidity(poolAddress, positionAddress) {
|
|
662
|
+
if (!this.identity) throw new Error('No Solana identity loaded');
|
|
663
|
+
|
|
664
|
+
let DLMM, BN;
|
|
665
|
+
try {
|
|
666
|
+
const dlmmMod = await import('@meteora-ag/dlmm');
|
|
667
|
+
DLMM = dlmmMod.default || dlmmMod.DLMM;
|
|
668
|
+
BN = (await import('bn.js')).default;
|
|
669
|
+
} catch {
|
|
670
|
+
throw new Error('Install @meteora-ag/dlmm @coral-xyz/anchor bn.js for Meteora liquidity');
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
console.log(`[Meteora] Removing liquidity from ${positionAddress}...`);
|
|
674
|
+
|
|
675
|
+
const pool = await DLMM.create(this.connection, new PublicKey(poolAddress));
|
|
676
|
+
const { userPositions } = await pool.getPositionsByUserAndLbPair(this.identity.publicKey);
|
|
677
|
+
const position = userPositions.find(p => p.publicKey.toBase58() === positionAddress);
|
|
678
|
+
if (!position) throw new Error('Position not found');
|
|
679
|
+
|
|
680
|
+
const binIds = position.positionData.positionBinData.map(b => b.binId);
|
|
681
|
+
|
|
682
|
+
const removeTx = await pool.removeLiquidity({
|
|
683
|
+
position: new PublicKey(positionAddress),
|
|
684
|
+
user: this.identity.publicKey,
|
|
685
|
+
fromBinId: binIds[0],
|
|
686
|
+
toBinId: binIds[binIds.length - 1],
|
|
687
|
+
liquiditiesBpsToRemove: new Array(binIds.length).fill(new BN(100 * 100)),
|
|
688
|
+
shouldClaimAndClose: true,
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
const txs = Array.isArray(removeTx) ? removeTx : [removeTx];
|
|
692
|
+
const signatures = [];
|
|
693
|
+
for (const tx of txs) {
|
|
694
|
+
const sig = await sendAndConfirmTransaction(this.connection, tx, [this.identity]);
|
|
695
|
+
signatures.push(sig);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
console.log(`[Meteora] Liquidity removed: ${signatures[0]}`);
|
|
699
|
+
return { success: true, txHashes: signatures, pool: poolAddress, position: positionAddress };
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
export default SolanaAutonomy;
|