This guide is for wallet service providers and developers who want to integrate
new key management solutions into the solana-keychain library. By adding your
signer implementation, you'll enable developers to use your service for secure
Solana transaction signing through a unified interface.
Keychain keeps its Rust, TypeScript, Python and Go implementations at parity. PRs that include all four are strongly preferred. If you can only contribute one, that's fine, but expect the others to be required before the signer ships in a release.
Using Claude Code? The repo's
add-signer-ci skill
wires up the CI workflows for a new signer contributed via a fork PR.
Architecture Overview
The library uses a trait-based architecture defined in src/traits.rs. Every
signer implements the SolanaSigner base trait (pubkey, sign_message,
is_available) plus exactly one capability trait matching the provider's shape:
TransactionSigner(sign_transaction): the provider signs the caller's bytes and the caller broadcasts. Most backends.ModifyingSigner(modify_and_sign_transaction): the provider rewrites the transaction, then signs it without broadcasting.SendingSigner(sign_and_send_transaction): the provider signs and broadcasts server-side.
The library also provides a unified Signer enum that wraps all
implementations, allowing runtime selection of signing backends. It implements
only the base trait; capabilities are reached through as_transaction_signer(),
as_modifying_signer() and as_sending_signer().
Quick Integration Checklist
- Create your signer module with implementation
- Implement the
SolanaSignerbase trait (pubkey,sign_message,is_available) plus the one capability trait matching your provider's shape (usuallyTransactionSigner) - Add a feature flag in
Cargo.toml, including the internal_remotemarker if your signer uses reqwest - Update the
Signerenum insrc/lib.rs(variant,dispatch_signer!arm, and an arm in the capability accessor matching your shape) - Enforce HTTPS and configure timeouts on HTTP clients
- Sign the message bytes and verify the returned signature before attaching it
- Add comprehensive unit tests (wiremock-based, in your module)
- Add an integration test file and update
.env.example - Add the TypeScript, Python and Go implementations
- Update documentation
- CI workflow updates (coordinate with maintainers, see CI Workflow Updates)
- Submit PR
Step 1: Create Your Signer Module
Create a new directory under src/ for your implementation:
src/├── your_service/│ ├── mod.rs # Main implementation with SolanaSigner trait│ └── types.rs # API request/response types (if needed)
Step 2: Define Your Signer Struct
In src/your_service/mod.rs, define your signer struct:
//! YourService API signer integrationuse crate::sdk_adapter::{Pubkey, Signature, VersionedTransaction};use crate::{error::SignerError, traits::SolanaSigner};use std::str::FromStr;/// YourService-based signer using YourService's API#[derive(Clone)]pub struct YourServiceSigner {api_key: String,api_secret: String,wallet_id: String,api_base_url: String,client: reqwest::Client,public_key: Pubkey,}impl std::fmt::Debug for YourServiceSigner {fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {f.debug_struct("YourServiceSigner").field("public_key", &self.public_key).finish_non_exhaustive()}}
Step 3: Implement Constructor and Helper Methods
Remote signers must enforce HTTPS and configure HTTP timeouts. Use the
shared HttpClientConfig struct for timeout settings.
use crate::http_client_config::HttpClientConfig;impl YourServiceSigner {pub fn new(api_key: String,api_secret: String,wallet_id: String,public_key: String,http_config: Option<HttpClientConfig>,) -> Result<Self, SignerError> {let pubkey = Pubkey::from_str(&public_key).map_err(|e| SignerError::InvalidPublicKey(format!("Invalid public key: {e}")))?;let http = http_config.unwrap_or_default();let builder = reqwest::Client::builder().timeout(http.resolved_request_timeout()).connect_timeout(http.resolved_connect_timeout());// Enforce HTTPS in production; wiremock uses HTTP in tests#[cfg(not(test))]let builder = builder.https_only(true);let client = builder.build().map_err(|e| {SignerError::ConfigError(format!("Failed to build HTTP client: {e}"))})?;Ok(Self {api_key,api_secret,wallet_id,api_base_url: "https://api.yourservice.com/v1".to_string(),client,public_key: pubkey,})}/// Sign raw bytes using your service's APIasync fn sign(&self, message: &[u8]) -> Result<Signature, SignerError> {let encoded_message = base64::engine::general_purpose::STANDARD.encode(message);let url = format!("{}/sign", self.api_base_url);let response = self.client.post(&url).header("Authorization", format!("Bearer {}", self.api_key)).json(&serde_json::json!({"wallet_id": self.wallet_id,"message": encoded_message,})).send().await?;// Use generic error messages: never expose raw API response textif !response.status().is_success() {let status = response.status().as_u16();return Err(SignerError::remote_api(format!("YourService API returned status {status}")));}// Parse response: always use map_err, never .expect() or .unwrap()let response_data: SignResponse = response.json().await.map_err(|e| SignerError::SerializationError(format!("Failed to parse response: {e}")))?;let sig_bytes = base64::engine::general_purpose::STANDARD.decode(&response_data.signature).map_err(|e| SignerError::SerializationError(format!("Failed to decode signature: {e}")))?;let sig_array: [u8; 64] = sig_bytes.try_into().map_err(|_| SignerError::SigningFailed("Invalid signature length".to_string()))?;Ok(Signature::from(sig_array))}}
Step 4: Implement the Signer Traits
The SolanaSigner base trait has 2 async methods (sign_message,
is_available) plus pubkey(). Transaction signing goes in a capability trait;
most providers implement TransactionSigner, whose sign_transaction returns
SignTransactionResult (a tagged enum marking the transaction fully or
partially signed).
Sign the message bytes, never the whole wire transaction, and verify the
provider's signature against them before attaching it. Use the shared
TransactionUtil helpers for signing and serialization.
use crate::signature_util::verify_or_reject;use crate::traits::{SignTransactionResult, TransactionSigner};use crate::transaction_util::TransactionUtil;#[async_trait::async_trait]impl SolanaSigner for YourServiceSigner {fn pubkey(&self) -> Pubkey {self.public_key}async fn sign_message(&self, message: &[u8]) -> Result<Signature, SignerError> {let signature = self.sign(message).await?;verify_or_reject(&signature, &self.public_key, message)?;Ok(signature)}async fn is_available(&self) -> bool {let url = format!("{}/health", self.api_base_url);self.client.get(&url).send().await.map(|r| r.status().is_success()).unwrap_or(false)}}#[async_trait::async_trait]impl TransactionSigner for YourServiceSigner {async fn sign_transaction(&self,tx: &mut VersionedTransaction,) -> Result<SignTransactionResult, SignerError> {let message = tx.message.serialize();let signature = self.sign(&message).await?;verify_or_reject(&signature, &self.public_key, &message)?;TransactionUtil::add_signature_to_transaction(tx, &self.public_key, signature)?;let serialized = TransactionUtil::serialize_transaction(tx)?;Ok(TransactionUtil::classify_signed_transaction(tx,(serialized, signature),))}}
If your provider rewrites the transaction before signing, implement
ModifyingSigner instead and replace the caller's transaction wholesale with
the provider's bytes. If it broadcasts server-side, implement SendingSigner
and leave the caller's transaction untouched. Never implement a capability your
provider cannot serve.
Step 5: Add API Types (Optional)
If your API needs custom types, create src/your_service/types.rs:
use serde::{Deserialize, Serialize};#[derive(Serialize)]pub struct SignRequest {pub wallet_id: String,pub message: String,}#[derive(Deserialize)]pub struct SignResponse {pub signature: String,}
Step 6: Add Feature Flag
Update Cargo.toml to add your signer as an optional feature. Remote backends
include the internal _remote marker, which gates the From<reqwest::Error>
impl in src/error.rs; without it, ? on reqwest calls won't compile when only
your feature is enabled.
[features]default = ["memory", "sdk-v2"]memory = []vault = ["dep:reqwest", "_remote"]privy = ["dep:reqwest", "dep:p256", "_remote"]your_service = ["dep:reqwest", "_remote"] # Add your featureall = ["memory", "vault", "privy", "...", "your_service"] # Update all
Step 7: Update the Signer Enum
Add your signer to src/lib.rs. The base SolanaSigner impl dispatches through
the dispatch_signer! macro, so add one arm there. Capability access lives in
the as_transaction_signer(), as_modifying_signer() and as_sending_signer()
accessors: add a Some(s) arm to the one matching your capability trait (and,
for a sending or modifying backend, a None arm in as_transaction_signer()).
// Add feature-gated module#[cfg(feature = "your_service")]pub mod your_service;// Re-export your signer type#[cfg(feature = "your_service")]pub use your_service::YourServiceSigner;// Add to Signer enum#[derive(Debug)]pub enum Signer {#[cfg(feature = "memory")]Memory(MemorySigner),// ... existing variants#[cfg(feature = "your_service")]YourService(YourServiceSigner), // Add your variant}// Add constructor methodimpl Signer {#[cfg(feature = "your_service")]pub fn from_your_service(api_key: String,api_secret: String,wallet_id: String,public_key: String,) -> Result<Self, SignerError> {Ok(Self::YourService(YourServiceSigner::new(api_key,api_secret,wallet_id,public_key,None, // uses default HttpClientConfig)?))}}// Update the dispatch macro: the base SolanaSigner impl uses it for// pubkey, sign_message and is_availablemacro_rules! dispatch_signer {($self:ident, $signer:pat => $body:expr) => {match $self {// ... existing variants#[cfg(feature = "your_service")]Signer::YourService($signer) => $body,}};}// Add your arm to the capability accessor matching your shapeimpl Signer {pub fn as_transaction_signer(&self) -> Option<&dyn TransactionSigner> {match self {// ... existing variants#[cfg(feature = "your_service")]Signer::YourService(s) => Some(s),}}}
Step 8: Add Comprehensive Tests
Add tests to your module (at the bottom of src/your_service/mod.rs):
#[cfg(test)]mod tests {use super::*;use solana_sdk::{signature::Keypair, signer::Signer};use wiremock::{matchers::{header, method, path},Mock, MockServer, ResponseTemplate,};#[tokio::test]async fn test_new() {let keypair = Keypair::new();let signer = YourServiceSigner::new("test-key".to_string(),"test-secret".to_string(),"test-wallet".to_string(),keypair.pubkey().to_string(),None,);assert!(signer.is_ok());}#[tokio::test]async fn test_sign_message() {let mock_server = MockServer::start().await;let keypair = Keypair::new();let message = b"test message";let signature = keypair.sign_message(message);Mock::given(method("POST")).and(path("/sign")).respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({"signature": base64::engine::general_purpose::STANDARD.encode(signature.as_ref())}))).expect(1).mount(&mock_server).await;let mut signer = YourServiceSigner::new("test-key".to_string(),"test-secret".to_string(),"test-wallet".to_string(),keypair.pubkey().to_string(),None,).unwrap();signer.api_base_url = mock_server.uri();let result = signer.sign_message(message).await;assert!(result.is_ok());}#[tokio::test]async fn test_sign_unauthorized() {let mock_server = MockServer::start().await;let keypair = Keypair::new();Mock::given(method("POST")).and(path("/sign")).respond_with(ResponseTemplate::new(401)).expect(1).mount(&mock_server).await;let mut signer = YourServiceSigner::new("bad-key".to_string(),"bad-secret".to_string(),"test-wallet".to_string(),keypair.pubkey().to_string(),None,).unwrap();signer.api_base_url = mock_server.uri();let result = signer.sign_message(b"test").await;assert!(result.is_err());}}
Also cover signature-verification failure: a mocked response whose signature does not verify must return an error.
Step 9: Add Integration Tests
Create rust/src/tests/test_<name>_integration.rs. Integration tests run
against the real service API (not wiremock) and are gated behind the
integration-tests feature. Each file needs:
pub constdeclarations for env var names- A
get_signer()helper that reads env vars viadotenvy - Three test functions:
test_<name>_sign_message,test_<name>_sign_transactionandtest_<name>_is_available - Feature gates:
#[cfg(feature = "your_service")]on the module and#[cfg(feature = "integration-tests")]on each test
See rust/src/tests/test_para_integration.rs for a complete reference, and
declare your module in rust/src/tests/mod.rs:
pub mod test_your_service_integration;
Step 10: Update Environment Variables
Update the root .env.example with your signer's env vars, following the
existing pattern:
# YourService Configuration (for SIGNER_TYPE=your_service)YOUR_SERVICE_API_KEY=your-api-keyYOUR_SERVICE_WALLET_ID=your-wallet-id# YOUR_SERVICE_API_BASE_URL=https://api.yourservice.com/v1 # Optional, defaults to this
List required vars first with placeholder values and optional vars commented out
with their defaults. Every env var used in integration tests must appear in
.env.example, and the TypeScript, Python and Go suites use the same names.
Step 11: Update Documentation
Add your signer to the supported backends table in README.md:
| Backend | Use Case | Feature Flag |
|---|---|---|
| Memory | Local keypairs, development, testing | memory |
| Vault | Enterprise key management with HashiCorp Vault | vault |
| Privy | Embedded wallets with Privy infrastructure | privy |
| Turnkey | Non-custodial key management via Turnkey | turnkey |
| YourService | Brief description of your service | your_service |
Also add your backend to the signer-capabilities table in each language's README. Add a usage example:
use solana_keychain::{Signer, SolanaSigner};#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> {let signer = Signer::from_your_service("your-api-key".to_string(),"your-api-secret".to_string(),"your-wallet-id".to_string(),"your-public-key".to_string(),)?;let pubkey = signer.pubkey();println!("Public key: {}", pubkey);Ok(())}
Testing Your Integration
Use the just recipes rather than raw cargo. The SDK features (sdk-v2,
sdk-v3, sdk-v4) are mutually exclusive, so cargo test --all-features
fails.
# Unit tests across the sdk-v2, sdk-v3 and sdk-v4 matricesjust rust-test# Live integration tests (require credentials in .env)just rust-test-integration
CI Workflow Updates (Fork PRs)
CI is a two-phase process. Coordinate with maintainers to prepare main before
your PR can be tested: they update fork-external-live-manual.yml, add env vars
to ci.yml, and configure GitHub Secrets.
Your PR must include these Phase 2 changes alongside your signer code:
ci.yml: add your Rust feature to thebackendmatrix (beforeall) and your integration test function to thetestmatrixtypescript-ci.yml: add your package to the unit and integration test matrices, and add env vars to the integration test steptypescript-publish.yml: add your package toPUBLISH_PACKAGES, the GitHub Releasepackagesarray, and the summary tablepython-ci.yml: add the backend to theCI_SIGNER_*gating inresolve-signersand to the integration matrixgo-ci.yml: add the backend to theCI_SIGNER_*gating and tomoduleBySigner
TypeScript Signer
If you're also adding a TypeScript signer package, create it at
typescript/packages/your-signer/. Key patterns:
- Factory function
createYourSigner()returnsSolanaTransactionSigner<TAddress> & SolanaMessageSigner<TAddress>; keep the signer class internal and export only the factory and its config type - Export config interface (
YourSignerConfig) - If your backend cannot sign messages, omit
SolanaMessageSignerand expose nosignMessagesmethod at all: Kit classifies signers by method presence, so a throwing stub would misclassify the signer - Enforce HTTPS on
apiBaseUrlconfig fields withassertHttpsUrl()from@solana/keychain-core - Make remote calls through
fetchSignerJson()from@solana/keychain-core, which owns error mapping, response sanitization, redirect rejection and the default timeout - Sanitize any other remote API error text with
sanitizeRemoteErrorResponse()from@solana/keychain-core - Use
throwSignerError(SignerErrorCode.*, { cause, message })from@solana/keychain-core - Honor
config?.abortSignalin every signing method, threading it intofetchSignerJson()andsignBatchStaggered() - Validate
requestDelayMswithvalidateRequestDelayMs()and batch withsignBatchStaggered() - Add
@throwsJSDoc to factory functions listing error codes - Declare
@solana/*packages as peer dependencies at>=8.1.0 - Unit tests with vitest and mocked
fetch; integration tests withrunSignerIntegrationTestfrom@solana/keychain-test-utils
Sending Signers
If your provider rewrites the transaction or broadcasts server-side, its
signature cannot be applied to the caller's transaction, so it must be a
SolanaSendingSigner instead of a SolanaTransactionSigner:
- Implement
signAndSendTransactions()and expose nosignTransactions(norsignMessages, unless the backend genuinely signs messages) - Honor
config?.abortSignal. Once the provider has accepted the create, report any failure (including an abort) asBROADCAST_UNCONFIRMEDcarrying the provider transaction id, with the abort reason kept ascause - Add a typed
createKeychainSigneroverload returning your sending-signer type - Exclude your backend from
KeychainKitPluginConfigintypescript/packages/kit-plugin/src/keychain-plugin.ts, since a sending signer cannot serve as a Kit clientpayeroridentity - Assert in unit tests that
isTransactionPartialSigner()is false andisTransactionSendingSigner()is true for your instances
Update Umbrella Package
Update typescript/packages/keychain/ (7 files to modify):
src/types.ts: AddYourSignerConfigto theKeychainSignerConfigdiscriminated unionsrc/create-keychain-signer.ts: Add the switch case (each backend package is loaded with a dynamicimport())src/resolve-address.ts: Add to the fast-path or fetch-path switch casesrc/index.ts: Add the config type, namespace and factory function exportspackage.json: Add@solana/keychain-your-signer: "workspace:*"dependencytsconfig.json: Add{ "path": "../your-signer" }referencetypescript/scripts/test-treeshake-umbrella.mjs: Add your package toSIGNER_MARKERSand your factory toFACTORIES
The switch statements have exhaustive never checks, so TypeScript will error
if you add to the union but miss a case. Run just ts-treeshake afterwards:
every package and the umbrella must stay tree-shakable.
Python Signer
Create the module at python/src/solana_keychain/your_signer/ (__init__.py
plus signer.py). Key patterns:
- Subclass
SolanaSignerfromsolana_keychain.core(pubkeyproperty,async sign_message(),async is_available()) plus exactly one capability class:TransactionSigner(async sign_transaction()),ModifyingSigner(async modify_and_sign_transaction()) orSendingSigner(async sign_and_send_transaction()) - Export an
async create_your_signer()factory that awaitsinit()when the backend resolves its address remotely; before init, signing raisesSignerErrorCode.NOT_INITIALIZED - Export a config dataclass (
YourSignerConfig) with secrets markedfield(repr=False) - Enforce HTTPS on base-URL fields with
assert_https_url(), and route every request throughfetch_signer_json() - Verify the provider's signature before returning it; use
signed_message_bytes()for the bytes a signature covers, never a hand-rolled version prefix - Register the backend in
_BACKENDSinpython/src/solana_keychain/keychain.py - If the backend needs a provider SDK, guard the import, raise an
ImportErrornamingpip install 'solana-keychain[your-signer]', declare the extra inpython/pyproject.toml, and never import the backend eagerly from the package root - Unit tests with pytest:
respxfor HTTP backends, stub clients for SDK backends, and a check that no secret appears instr()orrepr()of raised errors - Add a
make_your_signer()factory topython/tests/integration/test_live_signers.pyand register it in_MESSAGE_CAPABLE(or_TRANSACTION_ONLYfor backends withoutsign_message)
Go Signer
Each Go backend is its own module, at go/signers/yoursigner/ with module path
github.com/solana-foundation/solana-keychain/go/signers/yoursigner/v2. See
go/signers/para/ for a reference. Key patterns:
- Implement
core.SolanaSigner(Pubkey(),SignMessage(ctx, message),IsAvailable(ctx)) plus exactly one capability interface:core.TransactionSigner,core.ModifyingSignerorcore.SendingSigner. Capability is detected by type assertion, so a backend that cannot sign a transaction must carry noSignTransactionmethod at all - Export a
Configstruct and aNewconstructor (New(ctx, cfg)when the backend resolves its address remotely; before that, returncore.NewNotInitializedError) - Build HTTP clients with
core.NewHTTPClientorcore.ResolveHTTPClient, and validate base URLs withcore.NormalizeHTTPSBaseURL - Sign transactions with
core.SignTransactionWith, which signs the message bytes and attaches the signature; verify provider signatures withcore.VerifySignature - Return errors with
core.NewSignerError/core.WrapSignerErrorand stable codes such ascore.CodeSigningFailed - Unit tests with
httptest; integration tests inintegration_test.gobehind the//go:build integrationtag
Submission Checklist
Before submitting your PR:
- Code compiles without warnings (
just build) - All tests pass (
just test) - Code is formatted/linting passes (
just fmt) - No hardcoded values or secrets in code
- Error messages are generic (no raw API response text)
- No
.expect()or.unwrap()on untrusted API responses - HTTPS enforced on remote HTTP clients
- HTTP timeouts configured via
HttpClientConfig - Returned signatures verified against locally computed message bytes
- Follows naming conventions (snake_case for Rust and Python, camelCase for TypeScript)
- Integration tests added and
.env.exampleupdated - TypeScript, Python and Go implementations included (or planned with maintainers)
- Umbrella packages updated (TypeScript umbrella, Python
_BACKENDS) - CI changes included
- Added to README.md supported backends and signer-capabilities tables
Implementation Tips
Error Handling
Always use the existing SignerError variants. Both Display and Debug on
SignerError are redacted, so keep messages generic and never include raw API
response data. Never use .expect() or .unwrap() on untrusted API responses:
// Good: uses the constructor with a generic messagereturn Err(SignerError::remote_api(format!("YourService API returned status {status}")));// Good: converts from standard errorslet bytes = base64::decode(data).map_err(|e| SignerError::SerializationError(format!("Failed to decode: {e}")))?;
Security Requirements
These are not suggestions: consumers pick a backend on the strength of them. If
your provider makes one impossible, say so in the PR and in the repo's
docs/SECURITY_MODEL.md.
- Verify what the provider signed. Compute the message bytes locally and verify the returned signature against them at your signer's required position before attaching it. A signature that does not verify must fail, never be attached.
- Declare whether the provider broadcasts. If it executes server-side, implement the sending shape and return the signature that identifies the landed transaction. If the provider has no sign-only endpoint, make the backend sending-only rather than submitting behind the caller's back.
- Never leave the caller holding a transaction the signature does not cover. A provider that rewrites the transaction signs its own bytes: either leave the caller's transaction untouched (the broadcast-managed case), or take the modifying shape and replace it wholesale with the provider's bytes.
- Report broadcast uncertainty as such. A failure after submission may still
have landed. Surface
BROADCAST_UNCONFIRMEDrather than a generic error, carry the provider transaction id whenever you have one, and send a message-derived idempotency key on every create so a byte-identical resend is safe. - Never sign before init. Use
Option<Pubkey>(notPubkey::default()) for the public key beforeinit(), and fail with a config error if signing is attempted first. - Never log sensitive data (private keys, API secrets, raw API responses), and
use a
Debugimpl that hides sensitive fields - Enforce HTTPS (
https_only(true), gated with#[cfg(not(test))]for wiremock) and configure request and connect timeouts viaHttpClientConfig(defaults: 30s/5s) - Zeroize intermediate key material with
zeroize::Zeroizing<Vec<u8>>
Testing with Mocks
Use wiremock for mocking HTTP APIs. Assert on error type only, not error
message text:
#[cfg(test)]mod tests {use wiremock::{MockServer, Mock, ResponseTemplate};#[tokio::test]async fn test_api_call() {let mock_server = MockServer::start().await;Mock::given(method("POST")).respond_with(ResponseTemplate::new(200)).mount(&mock_server).await;// Use mock_server.uri() as your api_base_url}}
Getting Help
- Review existing signer implementations for patterns:
src/memory/mod.rs: Simple, synchronoussrc/privy/mod.rs: Requires initializationsrc/turnkey/mod.rs: Complex signature handlingsrc/vault/mod.rs: External client librarytypescript/packages/para/,python/src/solana_keychain/para/andgo/signers/para/: complete references in the other languages
- Key files:
src/traits.rs(trait definitions),src/transaction_util.rsandsrc/signature_util.rs(shared helpers),src/http_client_config.rs(timeout config) - The full repository guide: docs/ADDING_SIGNERS.md
- Open an issue for design discussions before starting work
Example PR Structure
feat(signer): add YourService signer integrationAdds support for YourService as a signing backend.- [X] Code compiles without warnings (`just build`)- [X] Code is formatted/linting passes (`just fmt`)- [X] Add comprehensive tests with wiremock - All tests pass (`just test`)- [X] Implemented SolanaSigner and TransactionSigner for YourServiceSigner- [X] Added feature flag 'your_service'- [X] HTTPS enforced, HTTP timeouts configured- [X] Returned signatures verified before attaching- [X] Added integration tests and updated .env.example- [X] TypeScript, Python and Go implementations included- [X] CI Phase 2 changes included- [X] Added to README.md supported backends tableCloses #1337
Is this page helpful?