Реализация подключения Snapshot для голосования DAO в мобильном приложении
Snapshot — это off-chain система голосования, где пользователь подписывает сообщение EIP-712 своим кошельком, а результат хранится в децентрализованной сети IPFS/Snapshot Hub. Голосование ничего не стоит — нет газа. При этом вес голоса определяется балансом токенов в блокчейне на момент создания предложения (snapshot block).
Интеграция Snapshot в мобильное приложение — это GraphQL API для чтения данных и REST API для отправки голосов с подписью.
Чтение данных: Snapshot GraphQL API
// Android — получение списка предложений Space через GraphQL
suspend fun getProposals(spaceId: String, first: Int = 20): List<SnapshotProposal> {
val query = """
{
proposals(
first: $first,
skip: 0,
where: { space: "$spaceId", state: "active" },
orderBy: "created",
orderDirection: desc
) {
id
title
body
choices
start
end
snapshot
state
scores
scores_total
votes
quorum
author
}
}
""".trimIndent()
return snapshotApi.query(query).data?.proposals ?: emptyList()
}
Endpoint: https://hub.snapshot.org/graphql. Space ID — уникальный идентификатор DAO в Snapshot (например, uniswap.eth, aave.eth).
Голосование: подпись EIP-712
Голос — это подписанное сообщение с типизированными данными:
// iOS — формирование и подпись голоса для Snapshot
func createVoteMessage(
proposalId: String,
choice: Int,
spaceId: String,
snapshotBlock: String
) -> TypedData {
return TypedData(
domain: TypedDataDomain(
name: "snapshot",
version: "0.1.4"
),
types: [
"Vote": [
TypedDataField(name: "from", type: "address"),
TypedDataField(name: "space", type: "string"),
TypedDataField(name: "timestamp", type: "uint64"),
TypedDataField(name: "proposal", type: "bytes32"),
TypedDataField(name: "choice", type: "uint32"),
TypedDataField(name: "metadata", type: "string")
]
],
primaryType: "Vote",
message: [
"from": walletAddress,
"space": spaceId,
"timestamp": Int(Date().timeIntervalSince1970),
"proposal": proposalId,
"choice": choice,
"metadata": "{}"
]
)
}
После получения подписи от пользователя — отправляем в Snapshot API:
// iOS — отправка подписанного голоса в Snapshot Hub
func submitVote(vote: VotePayload, sig: String) async throws {
let body = SnapshotVoteRequest(
address: walletAddress,
msg: jsonEncode(vote),
sig: sig
)
try await snapshotClient.post("/api/msg", body: body)
}
POST https://hub.snapshot.org/api/msg — endpoint для публикации. В ответ — id голоса (IPFS hash).
Стратегии голосования
Snapshot поддерживает кастомные стратегии определения веса голоса:
-
erc20-balance-of— стандартный баланс токена -
erc20-votes— делегированный вес черезgetVotes() -
delegation— с учётом входящих делегаций -
quadratic— квадратный корень из баланса - Можно комбинировать несколько стратегий
Стратегии читаются из конфигурации Space через GET https://hub.snapshot.org/api/spaces/{spaceId}. Показывай пользователю, какая стратегия используется и сколько голосов у него будет.
Расчёт веса голоса до голосования
// Android — получение голосующей силы через Snapshot Score API
suspend fun getVotingPower(
voter: String,
spaceId: String,
proposal: SnapshotProposal
): BigDecimal {
val response = snapshotScoreApi.getScores(
space = spaceId,
strategies = proposal.strategies,
network = proposal.network,
addresses = listOf(voter),
snapshot = proposal.snapshot.toLong()
)
return response.result.scores.firstOrNull()?.get(voter) ?: BigDecimal.ZERO
}
Endpoint: https://score.snapshot.org/api/scores. Показывай вес голоса прямо в форме голосования: «Ваш вес: 1 250.4 UNI».
Типы голосования Snapshot
| Тип | Описание |
|---|---|
single-choice |
Один вариант |
approval |
Несколько вариантов |
ranked-choice |
Ранжирование (IRV) |
quadratic |
Квадратичное голосование |
weighted |
Распределить вес по вариантам |
Для каждого типа — своя UI форма. weighted — слайдеры с процентами, сумма = 100%. ranked-choice — drag-and-drop или numbered list.
Сроки: 3–5 дней: GraphQL-запросы списка и деталей предложения, форма голосования с EIP-712 подписью через WalletConnect, отображение веса голоса, поддержка basic типов голосования (single-choice, approval).







