ПОШАГОВАЯ ИНСТРУКЦИЯ: ПРИМЕНЕНИЕ СМАРТ-КОНТРАКТА ЦФА ERACOIN
================================================================
ПОШАГОВАЯ ИНСТРУКЦИЯ: ПРИМЕНЕНИЕ СМАРТ-КОНТРАКТА ЦФА ERACOIN
В БЛОКЧЕЙНЕ ЭРАЧЕЙН (ERACHAIN)
================================================================
API документация: https://app.swaggerhub.com/apis-docs/Erachain/era-api/1.0.0-oas3
NPM пакет: erachain-js-api
GitHub: https://github.com/erachain
================================================================
════════════════════════════════════════════════════════════════
ШАГ 0. ПОДГОТОВКА ОКРУЖЕНИЯ
════════════════════════════════════════════════════════════════
0.1. Установка зависимостей
# Создаём проект
mkdir eracoin-cfa-project
cd eracoin-cfa-project
npm init -y
# Устанавливаем официальный API-клиент Эрачейн
npm install erachain-js-api
# Дополнительные пакеты
npm install axios crypto-js
0.2. Структура проекта
eracoin-cfa-project/
├── src/
│ ├── contracts/
│ │ └── eracoin_contract.js # Смарт-контракт
│ ├── config/
│ │ └── network.js # Настройки сети
│ ├── utils/
│ │ └── crypto.js # Криптографические утилиты
│ └── deploy.js # Скрипт деплоя
├── test/
│ └── contract.test.js # Тесты
├── .env # Переменные окружения
└── package.json
0.3. Файл конфигурации сети (src/config/network.js)
// ================================================================
// КОНФИГУРАЦИЯ СЕТЕЙ ЭРАЧЕЙН
// ================================================================
const NETWORKS = {
// TESTNET — для разработки и тестирования
testnet: {
name: 'Erachain Testnet',
baseUrl: 'https://testnet.erachain.org/api', // API endpoint
explorer: 'https://testnet.erachain.org',
chainId: 'testnet',
// Параметры ноды
node: {
host: 'testnet.erachain.org',
port: 9067, // Порт API
ssl: true
},
// Параметры блокчейна
blockTime: 30, // 30 секунд на блок
confirmations: 1, // Подтверждений для финальности
},
// MAINNET — боевой запуск
mainnet: {
name: 'Erachain Mainnet',
baseUrl: 'https://mainnet.erachain.org/api',
explorer: 'https://mainnet.erachain.org',
chainId: 'mainnet',
node: {
host: 'mainnet.erachain.org',
port: 9067,
ssl: true
},
blockTime: 30,
confirmations: 3,
},
// ЛОКАЛЬНАЯ НОДА (для разработки)
local: {
name: 'Local Erachain Node',
baseUrl: 'http://localhost:9067/api',
explorer: 'http://localhost:9067',
chainId: 'local',
node: {
host: 'localhost',
port: 9067,
ssl: false
},
blockTime: 30,
confirmations: 1,
}
};
module.exports = { NETWORKS };
════════════════════════════════════════════════════════════════
ШАГ 1. ПОДГОТОВКА СМАРТ-КОНТРАКТА
════════════════════════════════════════════════════════════════
1.1. Минимизированный контракт для вставки в поле script
// ================================================================
// СМАРТ-КОНТРАКТ ЦФА "ERACOIN" — ВЕРСИЯ ДЛЯ ВСТАВКИ В API
// ================================================================
// Этот код вставляется в поле "script" при создании актива через API
const ERACOIN_CONTRACT_SCRIPT = `
// === КОНФИГУРАЦИЯ ===
const CFG = {
NAME: "Цифровые облигации Eracoin серия 001",
TICKER: "ERACOIN001",
NOMINAL: 1000,
COUPON_RATE: 0.14,
COUPON_PERIOD: 3,
MATURITY_MONTHS: 24,
MIN_INVEST: 100000,
MAX_INVEST: 10000000,
ONLY_QUALIFIED: true,
EMITTER: "EMITTER_ADDRESS_PLACEHOLDER",
SETTLEMENT: "SETTLEMENT_ACCOUNT_PLACEHOLDER"
};
// === СОСТОЯНИЕ ===
const ST = {
holders: {},
coupons: [],
status: "CREATED",
issued: 0,
burned: 0,
totalPaid: 0,
frozen: {},
votes: {}
};
// === МОДИФИКАТОРЫ ===
function onlyEmitter() {
if (msg.sender !== CFG.EMITTER) throw "ACCESS_DENIED";
}
function onlyOperator() {
if (!isAuthOp(msg.sender)) throw "NOT_OPERATOR";
}
function onlyActive() {
if (ST.status !== "ACTIVE" && ST.status !== "COUPON_PAID") throw "NOT_ACTIVE";
}
function notFrozen(addr) {
if (ST.frozen[addr] && ST.frozen[addr].until > block.timestamp) throw "FROZEN";
}
function onlyQualified(addr) {
if (CFG.ONLY_QUALIFIED && !ST.holders[addr]?.qualified) throw "NOT_QUALIFIED";
}
// === KYC / РЕЕСТР ===
function registerInvestor(addr, kyc) {
onlyOperator();
if (ST.holders[addr]) throw "ALREADY_REGISTERED";
ST.holders[addr] = {
balance: 0,
kycVerified: true,
qualified: kyc.qualified || false,
regDate: block.timestamp,
totalReceived: 0,
lastCoupon: null
};
emit("INVESTOR_REGISTERED", {addr, qualified: kyc.qualified});
return {success: true};
}
function updateQualification(addr, isQ) {
onlyOperator();
if (!ST.holders[addr]) throw "NOT_FOUND";
ST.holders[addr].qualified = isQ;
emit("QUALIFICATION_UPDATED", {addr, qualified: isQ});
return {success: true};
}
// === ЭМИССИЯ ===
function primaryPlacement(recipients) {
onlyEmitter();
if (ST.status !== "CREATED") throw "ALREADY_PLACED";
let total = 0;
for (const r of recipients) {
const {address, amount} = r;
if (!ST.holders[address]) throw "NOT_REGISTERED";
onlyQualified(address);
notFrozen(address);
const newBal = ST.holders[address].balance + amount;
const invest = newBal * CFG.NOMINAL;
if (invest < CFG.MIN_INVEST) throw "MIN_INVESTMENT";
if (invest > CFG.MAX_INVEST) throw "MAX_INVESTMENT";
ST.holders[address].balance = newBal;
total += amount;
}
if (total > 50000) throw "OVER_ISSUE";
ST.issued = total;
ST.status = "ACTIVE";
emit("PLACEMENT_COMPLETE", {totalIssued: total});
return {success: true, totalIssued: total};
}
// === ПЕРЕВОДЫ ===
function transfer(from, to, amount) {
onlyActive();
if (msg.sender !== from && msg.sender !== CFG.EMITTER) throw "ACCESS_DENIED";
notFrozen(from);
notFrozen(to);
if (!ST.holders[to]) throw "NOT_REGISTERED";
onlyQualified(to);
if (ST.holders[from].balance < amount) throw "INSUFFICIENT";
const newTo = ST.holders[to].balance + amount;
if (newTo * CFG.NOMINAL > CFG.MAX_INVEST) throw "MAX_INVEST";
ST.holders[from].balance -= amount;
ST.holders[to].balance = newTo;
emit("TRANSFER", {from, to, amount});
return {success: true};
}
// === КУПОНЫ ===
function calcCoupon(addr) {
const h = ST.holders[addr];
if (!h || h.balance <= 0) return 0;
return Math.floor(h.balance * CFG.NOMINAL * CFG.COUPON_RATE / 4 * 100) / 100;
}
function payCoupons() {
onlyEmitter();
onlyActive();
const now = block.timestamp;
const last = ST.coupons.length > 0 ? ST.coupons[ST.coupons.length - 1].date : CFG.ISSUE_DATE;
const days = (now - last) / 86400;
if (days < 85) throw "TOO_EARLY";
let totalPaid = 0, count = 0;
const details = [];
for (const addr in ST.holders) {
const h = ST.holders[addr];
if (h.balance <= 0) continue;
const amt = calcCoupon(addr);
if (amt <= 0) continue;
h.totalReceived += amt;
h.lastCoupon = now;
totalPaid += amt;
count++;
details.push({addr, amt, bal: h.balance});
}
ST.coupons.push({date: now, amount: totalPaid, holders: count, details, tx: tx.hash});
ST.totalPaid += totalPaid;
ST.status = "COUPON_PAID";
emit("COUPONS_PAID", {total: totalPaid, count});
return {success: true, totalPaid, count};
}
function onSchedule(date) {
const next = getNextCouponDate();
if (Math.abs(date - next) < 86400) {
try { return payCoupons(); }
catch(e) { emit("SCHEDULED_FAIL", {error: e}); return {success: false, error: e}; }
}
return {success: true, msg: "Not time"};
}
function getNextCouponDate() {
const now = block.timestamp;
const issue = CFG.ISSUE_DATE;
const period = CFG.COUPON_PERIOD * 30 * 86400;
let next = issue + period;
while (next < now) next += period;
return next;
}
// === ПОГАШЕНИЕ ===
function earlyRedemption(addr, amount) {
onlyEmitter();
onlyActive();
notFrozen(addr);
const h = ST.holders[addr];
if (!h || h.balance < amount) throw "INSUFFICIENT";
const nominal = amount * CFG.NOMINAL;
const accrued = calcAccrued(addr);
const total = nominal + accrued;
h.balance -= amount;
ST.burned += amount;
emit("EARLY_REDEMPTION", {addr, amount, total});
return {success: true, total};
}
function onMaturity() {
const now = block.timestamp;
if (now < CFG.MATURITY_DATE) throw "NOT_MATURED";
if (ST.status === "MATURED") throw "ALREADY_MATURED";
let total = 0, count = 0;
for (const addr in ST.holders) {
const h = ST.holders[addr];
if (h.balance <= 0) continue;
const amt = h.balance;
const nominal = amt * CFG.NOMINAL;
const finalCoupon = calcCoupon(addr);
const payment = nominal + finalCoupon;
h.balance = 0;
ST.burned += amt;
total += payment;
count++;
emit("MATURITY_REDEMPTION", {addr, amount: amt, payment});
}
ST.status = "MATURED";
emit("MATURITY_COMPLETE", {total, count});
return {success: true, total, count};
}
function calcAccrued(addr) {
const h = ST.holders[addr];
if (!h || h.balance <= 0) return 0;
const days = (block.timestamp - (h.lastCoupon || CFG.ISSUE_DATE)) / 86400;
return Math.floor(h.balance * CFG.NOMINAL * CFG.COUPON_RATE * days / 365 * 100) / 100;
}
// === ЗАМОРОЗКА ===
function freezeAddress(addr, until, reason) {
onlyEmitter();
ST.frozen[addr] = {until, reason, at: block.timestamp};
emit("FROZEN", {addr, until, reason});
return {success: true};
}
function unfreezeAddress(addr) {
onlyEmitter();
delete ST.frozen[addr];
emit("UNFROZEN", {addr});
return {success: true};
}
// === ГОЛОСОВАНИЯ ===
function createVote(vid, topic, options, deadline) {
onlyEmitter();
if (ST.votes[vid]) throw "VOTE_EXISTS";
ST.votes[vid] = {topic, options, votes: {}, deadline, created: block.timestamp, status: "ACTIVE"};
emit("VOTE_CREATED", {vid, topic, deadline});
return {success: true};
}
function castVote(vid, optIdx) {
onlyActive();
notFrozen(msg.sender);
const v = ST.votes[vid];
if (!v) throw "VOTE_NOT_FOUND";
if (block.timestamp > v.deadline) throw "VOTE_CLOSED";
const h = ST.holders[msg.sender];
if (!h || h.balance <= 0) throw "NO_BALANCE";
if (optIdx < 0 || optIdx >= v.options.length) throw "INVALID_OPTION";
v.votes[msg.sender] = {optIdx, weight: h.balance, at: block.timestamp};
emit("VOTE_CAST", {vid, voter: msg.sender, optIdx, weight: h.balance});
return {success: true};
}
function tallyVotes(vid) {
onlyEmitter();
const v = ST.votes[vid];
if (!v) throw "VOTE_NOT_FOUND";
if (block.timestamp <= v.deadline) throw "VOTE_OPEN";
const res = {};
for (let i = 0; i < v.options.length; i++) res[i] = 0;
let total = 0;
for (const voter in v.votes) {
const vote = v.votes[voter];
res[vote.optIdx] += vote.weight;
total += vote.weight;
}
v.status = "CLOSED";
v.results = res;
v.totalVotes = total;
emit("VOTE_TALLIED", {vid, results: res, total});
return {success: true, results: res, total};
}
// === VIEW ФУНКЦИИ ===
function getBalance(addr) {
return ST.holders[addr]?.balance || 0;
}
function getHolderInfo(addr) {
const h = ST.holders[addr];
if (!h) return null;
return {
balance: h.balance,
qualified: h.qualified,
totalReceived: h.totalReceived,
currentCoupon: calcCoupon(addr),
accrued: calcAccrued(addr),
frozen: ST.frozen[addr] ? true : false
};
}
function getAssetInfo() {
return {
name: CFG.NAME,
ticker: CFG.TICKER,
nominal: CFG.NOMINAL,
quantity: 50000,
issued: ST.issued,
burned: ST.burned,
circulating: ST.issued - ST.burned,
couponRate: CFG.COUPON_RATE,
status: ST.status,
totalPaid: ST.totalPaid,
nextCoupon: getNextCouponDate()
};
}
// === ВСПОМОГАТЕЛЬНЫЕ ===
function isAuthOp(addr) {
// Проверка авторизации оператора ИС
return true; // Заменить на реальную проверку
}
function emit(event, data) {
log("EracoinEvent", {event, data, block: block.number, time: block.timestamp});
}
// === ОБРАБОТЧИКИ СОБЫТИЙ ===
function onBlock() {
const now = block.timestamp;
if (now >= CFG.MATURITY_DATE && ST.status !== "MATURED") {
try { onMaturity(); } catch(e) { emit("MATURITY_FAIL", {error: e}); }
}
const next = getNextCouponDate();
if (Math.abs(now - next) < 86400 && ST.status === "ACTIVE") {
try { payCoupons(); } catch(e) { emit("COUPON_FAIL", {error: e}); }
}
for (const addr in ST.frozen) {
if (ST.frozen[addr].until <= now) {
delete ST.frozen[addr];
emit("AUTO_UNFROZEN", {addr});
}
}
}
// === ИНИЦИАЛИЗАЦИЯ ===
function init(params) {
CFG.ISSUE_DATE = block.timestamp;
CFG.MATURITY_DATE = block.timestamp + 730 * 86400;
if (params.emitter) CFG.EMITTER = params.emitter;
if (params.settlement) CFG.SETTLEMENT = params.settlement;
emit("ASSET_CREATED", {name: CFG.NAME, ticker: CFG.TICKER});
return {success: true, assetId: CFG.TICKER + "_" + block.number};
}
module.exports = {init, registerInvestor, updateQualification, primaryPlacement, transfer, payCoupons, onSchedule, earlyRedemption, onMaturity, freezeAddress, unfreezeAddress, createVote, castVote, tallyVotes, getBalance, getHolderInfo, getAssetInfo, onBlock};
`;
module.exports = { ERACOIN_CONTRACT_SCRIPT };
════════════════════════════════════════════════════════════════
ШАГ 2. СКРИПТ ДЕПЛОЯ ЧЕРЕЗ API ЭРАЧЕЙН
════════════════════════════════════════════════════════════════
2.1. Основной скрипт деплоя (src/deploy.js)
// ================================================================
// СКРИПТ ДЕПЛОЯ ЦФА ERACOIN В БЛОКЧЕЙН ЭРАЧЕЙН
// ================================================================
const axios = require('axios');
const crypto = require('crypto-js');
const { NETWORKS } = require('./config/network');
const { ERACOIN_CONTRACT_SCRIPT } = require('./contracts/eracoin_contract');
// === КОНФИГУРАЦИЯ ===
const CONFIG = {
// Выбор сети: 'testnet' | 'mainnet' | 'local'
network: process.env.ERACHAIN_NETWORK || 'testnet',
// Ключи эмитента (хранить в .env!)
emitterSeed: process.env.EMITTER_SEED,
emitterAddress: process.env.EMITTER_ADDRESS,
// Параметры актива
asset: {
name: "Цифровые облигации Eracoin серия 001",
ticker: "ERACOIN001",
quantity: 50000,
scale: 2,
description: "Проспект: https://example.com/eracoin-prospectus | Купон 14% годовых | Срок 24 мес.",
movable: true,
divisible: true
}
};
// === КЛАСС API КЛИЕНТА ===
class ErachainAPI {
constructor(networkConfig) {
this.baseUrl = networkConfig.baseUrl;
this.node = networkConfig.node;
}
/**
* Получение информации о ноде
*/
async getNodeInfo() {
const response = await axios.get(`${this.baseUrl}/node/info`);
return response.data;
}
/**
* Получение баланса адреса
*/
async getBalance(address) {
const response = await axios.get(`${this.baseUrl}/addresses/balance/${address}`);
return response.data;
}
/**
* СОЗДАНИЕ АКТИВА (Asset) со смарт-контрактом
* POST /api/assets
*/
async createAsset(assetParams, seed) {
const payload = {
// Основные параметры актива
creator: assetParams.emitterAddress,
name: assetParams.name,
ticker: assetParams.ticker,
quantity: assetParams.quantity,
scale: assetParams.scale,
description: assetParams.description,
movable: assetParams.movable,
divisible: assetParams.divisible,
// === СМАРТ-КОНТРАКТ ===
// Вставляем JS-код в поле script
script: assetParams.script,
// Параметры инициализации контракта
scriptParams: {
emitter: assetParams.emitterAddress,
settlement: assetParams.settlementAccount
},
// Комиссия
fee: 1000000, // В минимальных единицах ERACHAIN
// Подпись
timestamp: Date.now()
};
// Подпись транзакции (пример — реальная реализация зависит от криптографии Эрачейн)
const signature = this.signTransaction(payload, seed);
payload.signature = signature;
const response = await axios.post(`${this.baseUrl}/assets`, payload, {
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.ERACHAIN_API_KEY
}
});
return response.data;
}
/**
* Вызов функции смарт-контракта
* POST /api/assets/{assetId}/call
*/
async callContract(assetId, functionName, params, seed) {
const payload = {
assetId: assetId,
function: functionName,
params: params,
caller: CONFIG.emitterAddress,
timestamp: Date.now()
};
const signature = this.signTransaction(payload, seed);
payload.signature = signature;
const response = await axios.post(
`${this.baseUrl}/assets/${assetId}/call`,
payload,
{ headers: { 'Content-Type': 'application/json' } }
);
return response.data;
}
/**
* Получение информации об активе
* GET /api/assets/{assetId}
*/
async getAsset(assetId) {
const response = await axios.get(`${this.baseUrl}/assets/${assetId}`);
return response.data;
}
/**
* Получение состояния смарт-контракта
* GET /api/assets/{assetId}/state
*/
async getContractState(assetId) {
const response = await axios.get(`${this.baseUrl}/assets/${assetId}/state`);
return response.data;
}
/**
* Подпись транзакции (заглушка — заменить на реальную криптографию Эрачейн)
*/
signTransaction(payload, seed) {
// В реальности используется Ed25519 или аналогичный алгоритм
const data = JSON.stringify(payload);
return crypto.SHA256(data + seed).toString();
}
}
// === ОСНОВНАЯ ЛОГИКА ДЕПЛОЯ ===
async function deployEracoin() {
console.log("╔═══════════════════════════════════════════════════════════════╗");
console.log("║ ДЕПЛОЙ ЦФА ERACOIN В БЛОКЧЕЙН ЭРАЧЕЙН ║");
console.log("╚═══════════════════════════════════════════════════════════════╝\n");
// 1. Инициализация API
const network = NETWORKS[CONFIG.network];
const api = new ErachainAPI(network);
console.log(`[1/8] Подключение к сети: ${network.name}`);
console.log(` URL: ${network.baseUrl}`);
// 2. Проверка подключения
try {
const nodeInfo = await api.getNodeInfo();
console.log(`[2/8] ✓ Нода доступна. Версия: ${nodeInfo.version}`);
console.log(` Высота блока: ${nodeInfo.height}`);
console.log(` Время блока: ${nodeInfo.blockTime}s`);
} catch (e) {
console.error(`[2/8] ✗ Ошибка подключения: ${e.message}`);
process.exit(1);
}
// 3. Проверка баланса эмитента
console.log(`[3/8] Проверка баланса эмитента: ${CONFIG.emitterAddress}`);
try {
const balance = await api.getBalance(CONFIG.emitterAddress);
console.log(` Баланс: ${balance.balance} ERACHAIN`);
if (balance.balance < 1000000) {
console.error(` ✗ Недостаточно средств для комиссии`);
process.exit(1);
}
console.log(` ✓ Баланс достаточен`);
} catch (e) {
console.error(` ✗ Ошибка: ${e.message}`);
process.exit(1);
}
// 4. Подготовка смарт-контракта
console.log(`[4/8] Подготовка смарт-контракта...`);
const script = ERACOIN_CONTRACT_SCRIPT
.replace('EMITTER_ADDRESS_PLACEHOLDER', CONFIG.emitterAddress)
.replace('SETTLEMENT_ACCOUNT_PLACEHOLDER', process.env.SETTLEMENT_ACCOUNT || '');
console.log(` ✓ Контракт подготовлен (${script.length} символов)`);
// 5. Создание актива
console.log(`[5/8] Создание актива в блокчейне...`);
console.log(` Название: ${CONFIG.asset.name}`);
console.log(` Тикер: ${CONFIG.asset.ticker}`);
console.log(` Объём: ${CONFIG.asset.quantity} шт.`);
console.log(` Номинал: 1000 RUB`);
console.log(` Купон: 14% годовых`);
let assetResult;
try {
assetResult = await api.createAsset({
...CONFIG.asset,
emitterAddress: CONFIG.emitterAddress,
settlementAccount: process.env.SETTLEMENT_ACCOUNT,
script: script
}, CONFIG.emitterSeed);
console.log(` ✓ Актив создан!`);
console.log(` Asset ID: ${assetResult.assetId}`);
console.log(` Tx Hash: ${assetResult.txHash}`);
console.log(` Block: ${assetResult.blockNumber}`);
} catch (e) {
console.error(` ✗ Ошибка создания: ${e.message}`);
if (e.response) {
console.error(` Детали: ${JSON.stringify(e.response.data)}`);
}
process.exit(1);
}
// 6. Ожидание подтверждения
console.log(`[6/8] Ожидание подтверждения (${network.confirmations} блоков)...`);
await new Promise(r => setTimeout(r, network.blockTime * network.confirmations * 1000));
console.log(` ✓ Подтверждено`);
// 7. Проверка актива
console.log(`[7/8] Проверка актива в блокчейне...`);
try {
const assetInfo = await api.getAsset(assetResult.assetId);
console.log(` ✓ Актив найден`);
console.log(` Статус: ${assetInfo.status}`);
console.log(` Скрипт: ${assetInfo.hasScript ? 'присутствует' : 'отсутствует'}`);
const state = await api.getContractState(assetResult.assetId);
console.log(` Состояние контракта: ${JSON.stringify(state, null, 2)}`);
} catch (e) {
console.error(` ✗ Ошибка проверки: ${e.message}`);
}
// 8. Инициализация контракта
console.log(`[8/8] Инициализация смарт-контракта...`);
try {
const initResult = await api.callContract(
assetResult.assetId,
'init',
{
emitter: CONFIG.emitterAddress,
settlement: process.env.SETTLEMENT_ACCOUNT
},
CONFIG.emitterSeed
);
console.log(` ✓ Контракт инициализирован`);
console.log(` Результат: ${JSON.stringify(initResult)}`);
} catch (e) {
console.error(` ✗ Ошибка инициализации: ${e.message}`);
}
// Итог
console.log("\n╔═══════════════════════════════════════════════════════════════╗");
console.log("║ ДЕПЛОЙ ЗАВЕРШЁН ║");
console.log("╠═══════════════════════════════════════════════════════════════╣");
console.log(`║ Asset ID: ${assetResult.assetId.padEnd(47)} ║`);
console.log(`║ Tx Hash: ${assetResult.txHash.padEnd(47)} ║`);
console.log(`║ Network: ${CONFIG.network.padEnd(47)} ║`);
console.log(`║ Explorer: ${(network.explorer + '/tx/' + assetResult.txHash).padEnd(47)} ║`);
console.log("╚═══════════════════════════════════════════════════════════════╝");
return assetResult;
}
// Запуск
if (require.main === module) {
deployEracoin().catch(console.error);
}
module.exports = { deployEracoin, ErachainAPI };
════════════════════════════════════════════════════════════════
ШАГ 3. ПОСЛЕДУЮЩИЕ ОПЕРАЦИИ С КОНТРАКТОМ
════════════════════════════════════════════════════════════════
3.1. Регистрация инвесторов (KYC)
// ================================================================
// РЕГИСТРАЦИЯ ИНВЕСТОРОВ (вызывается оператором ИС)
// ================================================================
async function registerInvestors(api, assetId, operatorSeed) {
const investors = [
{
address: "7NhZBb8CeLtcD1aBcDeFgHiJkLmNoPqRsTuVwXyZ",
kyc: {
name: "Иванов Иван Иванович",
passport: "4515 123456",
inn: "770123456789",
qualified: true // Квалифицированный инвестор
}
},
{
address: "8MiACc9DfMueE2bCdEfGhIjKlMnOpQrStUvWxYz",
kyc: {
name: "Петрова Мария Сергеевна",
passport: "4515 654321",
inn: "770987654321",
qualified: true
}
}
];
for (const investor of investors) {
console.log(`Регистрация инвестора: ${investor.kyc.name}`);
const result = await api.callContract(
assetId,
'registerInvestor',
{
address: investor.address,
kycData: investor.kyc
},
operatorSeed
);
console.log(` Результат: ${result.success ? '✓' : '✗'}`);
}
}
3.2. Первичное размещение
// ================================================================
// ПЕРВИЧНОЕ РАЗМЕЩЕНИЕ (вызывается эмитентом)
// ================================================================
async function primaryPlacement(api, assetId, emitterSeed) {
const recipients = [
{
address: "7NhZBb8CeLtcD1aBcDeFgHiJkLmNoPqRsTuVwXyZ",
amount: 10000 // 10 000 шт. × 1000 RUB = 10 000 000 RUB
},
{
address: "8MiACc9DfMueE2bCdEfGhIjKlMnOpQrStUvWxYz",
amount: 5000 // 5 000 шт. × 1000 RUB = 5 000 000 RUB
}
];
console.log("Первичное размещение ЦФА Eracoin...");
const result = await api.callContract(
assetId,
'primaryPlacement',
{ recipients: recipients },
emitterSeed
);
console.log(`Размещено: ${result.totalIssued} шт.`);
console.log(`Статус актива: ${result.status}`);
return result;
}
3.3. Выплата купонов
// ================================================================
// ВЫПЛАТА КУПОНОВ (автоматическая или ручная)
// ================================================================
async function payCoupons(api, assetId, emitterSeed) {
console.log("Выплата купонов...");
const result = await api.callContract(
assetId,
'payCoupons',
{},
emitterSeed
);
console.log(`Выплачено: ${result.totalPaid} RUB`);
console.log(`Инвесторов: ${result.count}`);
console.log(`Детали: ${JSON.stringify(result.paymentRecord, null, 2)}`);
return result;
}
3.4. Проверка информации об активе
// ================================================================
// ПРОВЕРКА СОСТОЯНИЯ АКТИВА
// ================================================================
async function checkAssetInfo(api, assetId) {
// Информация об активе
const assetInfo = await api.getAsset(assetId);
console.log("Информация об активе:", assetInfo);
// Состояние контракта
const state = await api.getContractState(assetId);
console.log("Состояние контракта:", state);
// Информация о конкретном владельце
const holderInfo = await api.callContract(
assetId,
'getHolderInfo',
{ address: "7NhZBb8CeLtcD1aBcDeFgHiJkLmNoPqRsTuVwXyZ" },
null // view функция не требует подписи
);
console.log("Информация о владельце:", holderInfo);
return { assetInfo, state, holderInfo };
}
════════════════════════════════════════════════════════════════
ШАГ 4. ПРОВЕРКА ЧЕРЕЗ EXPLORER / API
════════════════════════════════════════════════════════════════
4.1. Проверка через Explorer
https://testnet.erachain.org/tx/{TX_HASH}
https://testnet.erachain.org/asset/{ASSET_ID}
4.2. Проверка через REST API
# Информация об активе
curl -X GET "https://testnet.erachain.org/api/assets/ERACOIN001" -H "Content-Type: application/json"
# Состояние смарт-контракта
curl -X GET "https://testnet.erachain.org/api/assets/ERACOIN001/state" -H "Content-Type: application/json"
# История транзакций актива
curl -X GET "https://testnet.erachain.org/api/assets/ERACOIN001/transactions" -H "Content-Type: application/json"
# Баланс владельца
curl -X GET "https://testnet.erachain.org/api/addresses/balance/7NhZBb8CeLtcD1aBcDeFgHiJkLmNoPqRsTuVwXyZ" -H "Content-Type: application/json"
════════════════════════════════════════════════════════════════
ШАГ 5. ТЕСТИРОВАНИЕ НА TESTNET
════════════════════════════════════════════════════════════════
5.1. Тестовый скрипт
// ================================================================
// ТЕСТИРОВАНИЕ КОНТРАКТА НА TESTNET
// ================================================================
const { deployEracoin, ErachainAPI } = require('./src/deploy');
const { NETWORKS } = require('./src/config/network');
async function runTests() {
const network = NETWORKS.testnet;
const api = new ErachainAPI(network);
console.log("=== ТЕСТИРОВАНИЕ ЦФА ERACOIN ===\n");
// Тест 1: Деплой
console.log("Тест 1: Деплой контракта");
const asset = await deployEracoin();
// Тест 2: Регистрация инвестора
console.log("\nТест 2: Регистрация инвестора");
const regResult = await api.callContract(
asset.assetId,
'registerInvestor',
{
address: "TEST_ADDRESS_1",
kycData: { qualified: true }
},
process.env.OPERATOR_SEED
);
console.log(regResult.success ? "✓ Инвестор зарегистрирован" : "✗ Ошибка");
// Тест 3: Первичное размещение
console.log("\nТест 3: Первичное размещение");
const placeResult = await api.callContract(
asset.assetId,
'primaryPlacement',
{
recipients: [{ address: "TEST_ADDRESS_1", amount: 1000 }]
},
process.env.EMITTER_SEED
);
console.log(placeResult.success ? `✓ Размещено ${placeResult.totalIssued} шт.` : "✗ Ошибка");
// Тест 4: Проверка баланса
console.log("\nТест 4: Проверка баланса");
const balance = await api.callContract(
asset.assetId,
'getBalance',
{ address: "TEST_ADDRESS_1" },
null
);
console.log(`✓ Баланс: ${balance} ERACOIN`);
// Тест 5: Расчёт купона
console.log("\nТест 5: Расчёт купона");
const coupon = await api.callContract(
asset.assetId,
'calculateCoupon',
{ address: "TEST_ADDRESS_1" },
null
);
console.log(`✓ Купон: ${coupon} RUB`);
console.log("\n=== ВСЕ ТЕСТЫ ЗАВЕРШЕНЫ ===");
}
runTests().catch(console.error);
════════════════════════════════════════════════════════════════
ШАГ 6. ПЕРЕХОД НА MAINNET
════════════════════════════════════════════════════════════════
6.1. Чек-лист перед mainnet
□ Пройден аудит смарт-контракта (внешний аудитор)
□ Тесты на testnet пройдены успешно (100%)
□ Подготовлены все юридические документы (проспект, договор с ОИС)
□ Получена лицензия ЦБ РФ на выпуск ЦФА
□ Выбран и зарегистрирован оператор ИС в реестре ЦБ РФ
□ Подготовлен расчётный счёт для купонных выплат
□ Настроен KYC/AML процесс
□ Подготовлена инфраструктура для инвесторов
□ Создана резервная копия приватных ключей
□ Настроен мониторинг и алерты
6.2. Команда для mainnet
# Установка переменных окружения
export ERACHAIN_NETWORK=mainnet
export EMITTER_SEED="your-secret-seed-phrase"
export EMITTER_ADDRESS="your-emitter-address"
export SETTLEMENT_ACCOUNT="40802810..."
export ERACHAIN_API_KEY="your-api-key"
# Запуск деплоя
node src/deploy.js
════════════════════════════════════════════════════════════════
ПРИЛОЖЕНИЕ: API ENDPOINTS ЭРАЧЕЙН (SWAGGER)
════════════════════════════════════════════════════════════════
Основные endpoints (по документации Swagger):
| Method |
Endpoint |
Описание |
| GET |
/api/node/info |
Информация о ноде |
| GET |
/api/addresses/balance/{address} |
Баланс адреса |
| POST |
/api/assets |
Создание актива (с контрактом) |
| GET |
/api/assets/{assetId} |
Информация об активе |
| GET |
/api/assets/{assetId}/state |
Состояние контракта |
| POST |
/api/assets/{assetId}/call |
Вызов функции контракта |
| GET |
/api/assets/{assetId}/transactions |
История транзакций |
| POST |
/api/transactions |
Отправка транзакции |
| GET |
/api/blocks/{height} |
Информация о блоке |
| GET |
/api/blocks/last |
Последний блок |
Формат ответа API:
{
"success": true,
"data": { ... },
"timestamp": 1717189200000,
"blockHeight": 1234567
}
No comments to display
No comments to display